Go 单元测试之HTTP哀求与API测试

打印 上一主题 下一主题

主题 896|帖子 896|积分 2688

目次

一、httptest

1.1 前置代码准备

假设我们的业务逻辑是搭建一个http server端,对外提供HTTP服务。用来处理用户登录哀求,用户需要输入邮箱,密码。
  1. package main
  2. import (
  3.         regexp "github.com/dlclark/regexp2"
  4.         "github.com/gin-gonic/gin"
  5.         "net/http"
  6. )
  7. type UserHandler struct {
  8.         emailExp    *regexp.Regexp
  9.         passwordExp *regexp.Regexp
  10. }
  11. func (u *UserHandler) RegisterRoutes(server *gin.Engine) {
  12.         ug := server.Group("/user")
  13.         ug.POST("/login", u.Login)
  14. }
  15. func NewUserHandler() *UserHandler {
  16.         const (
  17.                 emailRegexPattern    = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$"
  18.                 passwordRegexPattern = `^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$`
  19.         )
  20.         emailExp := regexp.MustCompile(emailRegexPattern, regexp.None)
  21.         passwordExp := regexp.MustCompile(passwordRegexPattern, regexp.None)
  22.         return &UserHandler{
  23.                 emailExp:    emailExp,
  24.                 passwordExp: passwordExp,
  25.         }
  26. }
  27. type LoginRequest struct {
  28.         Email string `json:"email"`
  29.         Pwd   string `json:"pwd"`
  30. }
  31. func (u *UserHandler) Login(ctx *gin.Context) {
  32.         var req LoginRequest
  33.         if err := ctx.ShouldBindJSON(&req); err != nil {
  34.                 ctx.JSON(http.StatusBadRequest, gin.H{"msg": "参数不正确!"})
  35.                 return
  36.         }
  37.         // 校验邮箱和密码是否为空
  38.         if req.Email == "" || req.Pwd == "" {
  39.                 ctx.JSON(http.StatusBadRequest, gin.H{"msg": "邮箱或密码不能为空"})
  40.                 return
  41.         }
  42.         // 正则校验邮箱
  43.         ok, err := u.emailExp.MatchString(req.Email)
  44.         if err != nil {
  45.                 ctx.JSON(http.StatusInternalServerError, gin.H{"msg": "系统错误!"})
  46.                 return
  47.         }
  48.         if !ok {
  49.                 ctx.JSON(http.StatusBadRequest, gin.H{"msg": "邮箱格式不正确"})
  50.                 return
  51.         }
  52.         // 校验密码格式
  53.         ok, err = u.passwordExp.MatchString(req.Pwd)
  54.         if err != nil {
  55.                 ctx.JSON(http.StatusInternalServerError, gin.H{"msg": "系统错误!"})
  56.                 return
  57.         }
  58.         if !ok {
  59.                 ctx.JSON(http.StatusBadRequest, gin.H{"msg": "密码必须大于8位,包含数字、特殊字符"})
  60.                 return
  61.         }
  62.         // 校验邮箱和密码是否匹配特定的值来确定登录成功与否
  63.         if req.Email != "123@qq.com" || req.Pwd != "hello#world123" {
  64.                 ctx.JSON(http.StatusBadRequest, gin.H{"msg": "邮箱或密码不匹配!"})
  65.                 return
  66.         }
  67.         ctx.JSON(http.StatusOK, gin.H{"msg": "登录成功!"})
  68. }
  69. func InitWebServer(userHandler *UserHandler) *gin.Engine {
  70.         server := gin.Default()
  71.         userHandler.RegisterRoutes(server)
  72.         return server
  73. }
  74. func main() {
  75.         uh := &UserHandler{}
  76.         server := InitWebServer(uh)
  77.         server.Run(":8080") // 在8080端口启动服务器
  78. }
复制代码
1.2 介绍

在 Web 开辟场景下,单元测试常常需要模拟 HTTP 哀求和响应。利用 httptest 可以让我们在测试代码中创建一个 HTTP 服务器实例,并界说特定的哀求和响应行为,从而模拟真实世界的网络交互,在Go语言中,一般都推荐利用Go尺度库 net/http/httptest 进行测试。
1.3 根本用法

利用 httptest 的根本步骤如下:

  • 导入 net/http/httptest 包。
  • 创建一个 httptest.Server 实例,并指定你想要的服务器行为。
  • 在测试代码中利用 httptest.NewRequest 创建一个模拟的 HTTP 哀求,并将其发送到 httptest.Server。
  • 检查响应内容或状态码是否符合预期。
以下是一个简单的 httptest 用法示例
  1. package main
  2. import (
  3.         "bytes"
  4.         "github.com/gin-gonic/gin"
  5.         "github.com/stretchr/testify/assert"
  6.         "net/http"
  7.         "net/http/httptest"
  8.         "testing"
  9. )
  10. func TestUserHandler_Login(t *testing.T) {
  11.         // 定义测试用例
  12.         testCases := []struct {
  13.                 name     string
  14.                 reqBody  string
  15.                 wantCode int
  16.                 wantBody string
  17.         }{
  18.                 {
  19.                         name:     "登录成功",
  20.                         reqBody:  `{"email": "123@qq.com", "pwd": "hello#world123"}`,
  21.                         wantCode: http.StatusOK,
  22.                         wantBody: `{"msg": "登录成功!"}`,
  23.                 },
  24.                 {
  25.                         name:     "参数不正确",
  26.                         reqBody:  `{"email": "123@qq.com", "pwd": "hello#world123",}`,
  27.                         wantCode: http.StatusBadRequest,
  28.                         wantBody: `{"msg": "参数不正确!"}`,
  29.                 },
  30.                 {
  31.                         name:     "邮箱或密码为空",
  32.                         reqBody:  `{"email": "", "pwd": ""}`,
  33.                         wantCode: http.StatusBadRequest,
  34.                         wantBody: `{"msg": "邮箱或密码不能为空"}`,
  35.                 },
  36.                 {
  37.                         name:     "邮箱格式不正确",
  38.                         reqBody:  `{"email": "invalidemail", "pwd": "hello#world123"}`,
  39.                         wantCode: http.StatusBadRequest,
  40.                         wantBody: `{"msg": "邮箱格式不正确"}`,
  41.                 },
  42.                 {
  43.                         name:     "密码格式不正确",
  44.                         reqBody:  `{"email": "123@qq.com", "pwd": "invalidpassword"}`,
  45.                         wantCode: http.StatusBadRequest,
  46.                         wantBody: `{"msg": "密码必须大于8位,包含数字、特殊字符"}`,
  47.                 },
  48.                 {
  49.                         name:     "邮箱或密码不匹配",
  50.                         reqBody:  `{"email": "123123@qq.com", "pwd": "hello#world123"}`,
  51.                         wantCode: http.StatusBadRequest,
  52.                         wantBody: `{"msg": "邮箱或密码不匹配!"}`,
  53.                 },
  54.         }
  55.         for _, tc := range testCases {
  56.                 t.Run(tc.name, func(t *testing.T) {
  57.                         // 创建一个 gin 的上下文
  58.                         server := gin.Default()
  59.                         h := NewUserHandler()
  60.                         h.RegisterRoutes(server)
  61.                         // mock 创建一个 http 请求
  62.                         req, err := http.NewRequest(
  63.                                 http.MethodPost,                     // 请求方法
  64.                                 "/user/login",                       // 请求路径
  65.                                 bytes.NewBuffer([]byte(tc.reqBody)), // 请求体
  66.                         )
  67.                         // 断言没有错误
  68.                         assert.NoError(t, err)
  69.                         // 设置请求头
  70.                         req.Header.Set("Content-Type", "application/json")
  71.                         // 创建一个响应
  72.                         resp := httptest.NewRecorder()
  73.                         // 服务端处理请求
  74.                         server.ServeHTTP(resp, req)
  75.                         // 断言响应码和响应体
  76.                         assert.Equal(t, tc.wantCode, resp.Code)
  77.                         // 断言 JSON 字符串是否相等
  78.                         assert.JSONEq(t, tc.wantBody, resp.Body.String())
  79.                 })
  80.         }
  81. }
复制代码
在这个例子中,我们创建了一个简单的 HTTP 哀求,TestUserHandler_Login 函数界说了一个测试函数,用于测试用户登录功能的差别情况。

  • testCases 列表界说了多个测试用例,每个测试用例包含了测试名称、哀求体、期望的 HTTP 状态码和期望的响应体内容。
  • 利用 for 循环遍历测试用例列表,每次循环创建一个新的测试子函数,并在此中模拟 HTTP 哀求发送给登录接口。
  • 在每个测试子函数中,先创建一个 Gin 的默认上下文和用户处理器 UserHandler,然后注册路由并创建一个模拟的 HTTP 哀求。
  • 通过 httptest.NewRecorder() 创建一个响应记录器,利用 server.ServeHTTP(resp, req) 处理模拟哀求,得到响应结果。
  • 末了利用断言来验证实际响应的 HTTP 状态码和响应体是否与测试用例中的期望一致。
末了,利用Goland 运行测试,结果如下:

二、gock

2.1介绍

gock 可以资助你在测试过程中模拟 HTTP 哀求和响应,这对于测试涉及外部 API 调用的应用程序非常有用。它可以让你轻松地界说模拟哀求,并验证你的应用程序是否精确处理了这些哀求。
GitHub 地点:github.com/h2non/gock
2.2 安装

你可以通过以下方式安装 gock:
  1. go get -u github.com/h2non/gock
复制代码
导入 gock 包:
  1. import "github.com/h2non/gock"
复制代码
2.3 根本利用

gock 的根本用法如下:

  • 启动拦截器:在测试开始前,利用 gock.New 函数启动拦截器,并指定你想要拦截的域名和端口。
  • 界说拦截规则:你可以利用 gock.Intercept 方法来界说拦截规则,比如拦截特定的 URL、方法、头部信息等。
  • 设置响应:你可以利用 gock.NewJson、gock.NewText 等方法来设置拦截后的响应内容。
  • 运行测试:在界说了拦截规则和响应后,你可以运行测试,gock 会拦截你的 HTTP 哀求,并返回你设置的响应。
2.4 举个例子

2.4.1 前置代码

如果我们是在代码中哀求外部API的场景(比如通过API调用其他服务获取返回值)又该怎么编写单元测试呢?
例如,我们有以下业务逻辑代码,依赖外部API:http://your-api.com/post提供的数据。
  1. // ReqParam API请求参数
  2. type ReqParam struct {
  3.         X int `json:"x"`
  4. }
  5. // Result API返回结果
  6. type Result struct {
  7.         Value int `json:"value"`
  8. }
  9. func GetResultByAPI(x, y int) int {
  10.         p := &ReqParam{X: x}
  11.         b, _ := json.Marshal(p)
  12.         // 调用其他服务的API
  13.         resp, err := http.Post(
  14.                 "http://your-api.com/post",
  15.                 "application/json",
  16.                 bytes.NewBuffer(b),
  17.         )
  18.         if err != nil {
  19.                 return -1
  20.         }
  21.         body, _ := ioutil.ReadAll(resp.Body)
  22.         var ret Result
  23.         if err := json.Unmarshal(body, &ret); err != nil {
  24.                 return -1
  25.         }
  26.         // 这里是对API返回的数据做一些逻辑处理
  27.         return ret.Value + y
  28. }
复制代码
在对类似上述这类业务代码编写单元测试的时间,如果不想在测试过程中真正去发送哀求或者依赖的外部接口还没有开辟完成时,我们可以在单元测试中对依赖的API进行mock。
2.4.2 测试用例

利用gock对外部API进行mock,即mock指定参数返回约定好的响应内容。 下面的代码中mock了两组数据,构成了两个测试用例。
  1. package gock_demo
  2. import (
  3.         "testing"
  4.         "github.com/stretchr/testify/assert"
  5.         "gopkg.in/h2non/gock.v1"
  6. )
  7. func TestGetResultByAPI(t *testing.T) {
  8.         defer gock.Off() // 测试执行后刷新挂起的mock
  9.         // mock 请求外部api时传参x=1返回100
  10.         gock.New("http://your-api.com").
  11.                 Post("/post").
  12.                 MatchType("json").
  13.                 JSON(map[string]int{"x": 1}).
  14.                 Reply(200).
  15.                 JSON(map[string]int{"value": 100})
  16.         // 调用我们的业务函数
  17.         res := GetResultByAPI(1, 1)
  18.         // 校验返回结果是否符合预期
  19.         assert.Equal(t, res, 101)
  20.         // mock 请求外部api时传参x=2返回200
  21.         gock.New("http://your-api.com").
  22.                 Post("/post").
  23.                 MatchType("json").
  24.                 JSON(map[string]int{"x": 2}).
  25.                 Reply(200).
  26.                 JSON(map[string]int{"value": 200})
  27.         // 调用我们的业务函数
  28.         res = GetResultByAPI(2, 2)
  29.         // 校验返回结果是否符合预期
  30.         assert.Equal(t, res, 202)
  31.         assert.True(t, gock.IsDone()) // 断言mock被触发
  32. }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

知者何南

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表