Golang单元测试 | 测试经理质疑我不会自测?
一、背景
测试驱动开发(Test-Driven Development,简称TDD)是一种软件开发的理念,尤其在后端开发中广泛应用,其核心思想是在实际编写任何业务逻辑代码之前,先编写测试用例。
单元测试:针对每一个方法进行的测试,单独验证每一个方法的正确性。
集成测试:多个组件合并在一起的测试,验证各个方法、组件之间配合无误。
所以我们要先搞单元测试,单元测试初步验证之后,再集成测试。而单元测试和集成测试是要求开发人员自测的,不是测试的task(狗头保命)。【本文讲单元测试(以测 Handler 层的方法为例),后续会更新集成测试】
二、Table Driven模式
Go 里面,惯常的组织测试的方式,都是用 Table Driven
。其组织结构如下图,共三部分:1)定义测试用例 2)具体测试用例 3)执行测试用例
你把测试用例定义看做是列名,每一个测试用例就是一行数据,就能理解 Table Driven
这个含义了。
三、mock
单元测试不需要再构造 UserHandler
所依赖的 Service
(如 UserService
和 CodeService
),而是用 mock 工具生成测试用的模拟的 UserService
和 CodeService
。
mock 共两部分:
- mockgen:命令行工具。安装:
go install go.uber.org/mock/mockgen@latest
- 测试中使用的控制 mock 对象的包。安装:
https://github.com/uber-go/mock
四、测试Handler
1)为UserService
和 CodeService
提供 mock 实现,执行如下命令,生成下图的 mock 文件
mockgen `-source=./webook/internal/service/article.go `-package=svcmocks `-destination=./webook/internal/service/mocks/article.mock.go
test文件的实现:
package web
import (
"bytes"
"encoding/json"
"errors"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"net/http"
"net/http/httptest"
"refactor-webook/webook/internal/domain"
"refactor-webook/webook/internal/service"
svcmocks "refactor-webook/webook/internal/service/mocks"
ijwt "refactor-webook/webook/internal/web/jwt"
"testing"
)
func TestUserHandler_SignUp(t *testing.T) {
testCases := []struct {
name string
mock func(ctrl *gomock.Controller) (service.UserService, service.CodeService, ijwt.Handler)
// 预期中的输入
reqBuilder func(t *testing.T) *http.Request
// 预期中的输出
wantCode int
wantBody Result
}{
// 定义测试用例
// note 先测试正常的流程(最长的流程),再考虑异常流程
{
name: "注册成功",
mock: func(ctrl *gomock.Controller) (service.UserService, service.CodeService, ijwt.Handler) {
userSvc := svcmocks.NewMockUserService(ctrl)
userSvc.EXPECT().SignUp(gomock.Any(), domain.User{
Email: "123@qq.com",
Password: "hello!123",
}).Return(nil)
// note CodeSvc没有用到的话,也可以直接写nil
// return userSvc, nil
codeSvc := svcmocks.NewMockCodeService(ctrl)
return userSvc, codeSvc, nil
},
reqBuilder: func(t *testing.T) *http.Request {
req, err := http.NewRequest(http.MethodPost, "/users/signup", bytes.NewReader(
[]byte(`{"email": "123@qq.com", "password": "hello!123", "confirmPassword":"hello!123"}`)))
// note 易漏:为req的header添加content-type(因为Bind方法)
req.Header.Set("Content-Type", "application/json")
assert.NoError(t, err)
return req
},
wantCode: http.StatusOK,
wantBody: Result{
Msg: "注册成功",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// 1. 构造ctrl
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// 2. mock出来需要的svc
userSvc, codeSvc, jwtHdl := tc.mock(ctrl)
// 3. 构造hdl
hdl := NewUserHandler(userSvc, codeSvc, jwtHdl)
// 4.
// 准备服务器和注册路由
server := gin.Default()
hdl.RegisterRoutes(server)
// 准备请求和响应
req := tc.reqBuilder(t)
recorder := httptest.NewRecorder()
// 本地接收http请求
server.ServeHTTP(recorder, req)
// 断言结果
assert.Equal(t, tc.wantCode, recorder.Code)
// 对res反序列化
var res Result
err := json.NewDecoder(recorder.Body).Decode(&res)
assert.NoError(t, err)
assert.Equal(t, tc.wantBody, res)
})
}
}
五、总结
- 测试 Handler 的难点在于“执行测试用例”中的 构造请求和响应。
- 依赖注入和面向接口编程可以方便执行单元测试。
- 测试用例先写正常分支(执行路径最长的分支),然后再写异常分支,要尽量涵盖所有可能的分支。
转载自:https://juejin.cn/post/7386552052933410856