单测投入成本以及收益
1. 单测成本一开始投入极大, 但随着时间的推移、经验的积累单测成本/研发投入成本,在逐渐降低。
2. 单测一定程度上降低了提测bug数,提升对代码质量的信心。
如何写单测
SmartUtil
优点
1. 快速生成模版代码。
2. 针对路径中需要mock 方法(第一层函数调用、数据库、redis) 会自动识别出来进行生成。
3. Goland/Vscode 本地集成 非常便捷。
针对函数生成Mock语句(强烈推荐)(Goland 版本1.7.6及以上支持)
在单测初期或使用新mock框架后,我们不知道如何写对下游依赖函数进行Mock,需要大量时间来学习手册,本功能针对被Mock的函数支持一键生成mock语句;
一个自动生成的简单例子
// we create the test template for the runnable function
// please fill the testcase and mock function
func Test_doTempSaveFormInstance_JDHFSU(t *testing.T) {
type Args struct {
Ctx context.Context
RenderEntity *render_entity.RenderEntity
FormInstance *model.FormInstance
FieldValueTOList []*form.FieldValueTO
TempVersionID int64
}
type test struct {
Name string
Args Args
Want *int64
WantErr bool
}
tests := []test{
// TODO: add the testcase
}
for _, tt := range tests {
mockey.PatchConvey(tt.Name, t, func() {
// TODO: add the return of mock functions
mockey.Mock(render_entity.FilterFieldValueTO).Return().Build()
mockey.Mock(value_check.CheckFieldValueTOList).Return().Build()
mockey.Mock(getTempSaveFormExpressionValueList).Return().Build()
mockey.Mock(feature_gating.EnableMultiEdit).Return().Build()
mockey.Mock(id_generator.GetGenerateID).Return().Build()
mockey.Mock(log.CtxErrorf).Return().Build()
mockey.Mock(getOrCreateFormInstanceEncryptedKey).Return().Build()
mockey.Mock(mysql.GetDB).Return().Build()
mockey.Mock((*gorm.DB).Transaction).Return().Build()
got, err := doTempSaveFormInstance(tt.Args.Ctx, tt.Args.RenderEntity, tt.Args.FormInstance, tt.Args.FieldValueTOList, tt.Args.TempVersionID)
if (err != nil) != tt.WantErr {
t.Errorf("%q. doTempSaveFormInstance() error = %v, wantErr %v", tt.Name, err, tt.WantErr)
}
if got != tt.Want {
t.Errorf("%q. doTempSaveFormInstance() = %v, want %v", tt.Name, got, tt.Want)
}
})
}
}
单测工具
Go Test
断言(verify)
基本语法
断言 go convey
常用的方式
convey.So(var1, ShouldEqual, var2) // 断言 var1 == var2
convey.So(err, ShouldBeNil) // 断言 err == nil
convey.So(err, ShouldBeErr, expectErr) // 断言 err == expectErr
convey.So(slice1, ShouldResemble, slice2) // 断言两个切片相等
convey.So(var1, ShouldBeZeroValue) // 断言 var 是该类型的零值
convey.So(num1, ShouldBeLessThan, 2) // 断言 num1 < 2
convey.So(got, convey.ShouldBeTrue) // 断言是否为true
自定义断言方式
比如: 判断一个Map的Key是否与另一个Map的Value相同
func MyMapCompare(actual interface{}, expected ...interface{}) string {
map1 := actual(map[string]string)
map2 := expected[0](map[string]string)
if len(map1) != len(map2) {
return "length of map is not equal to another"
}
for _, val := range map2 {
if _, ok := map1[val]; !ok {
return "the val in map2 is not the key in map1";
}
return ""
}
// 使用自定义断言函数
So(map1, MyMapCompare, map2)
断言写法
// 这样执行的时候会把case 的名字打印出来
convey.Convey(tt.Name, func() { convey.So(got, convey.ShouldResemble, testArgs.Want) })
//或者
convey.SoMsg(tt.Name, got, convey.ShouldResemble, testArgs.Want)
执行
go test -gcflags="all=-l -N" -v
Mock 函数
mock := func(ctx context.Context, nationalIDType *form.ValueTO, nationalIDValueTO *form.ValueTO) (bool, error) {
reg, err := regexp.Compile(checkRule)
if err != nil {
return false, nil
}
nationalID, err := utils.StringFormValue(nationalIDValueTO)
if err != nil {
return false, nil
}
return reg.Match([]byte(nationalID)), nil
}
mockey.Mock(details_widget_service.ValidateNationalID).To(mock).Build()
Mock struct函数
type Class struct{}
func (c *Class) FunB (s string) string {
...
return s
}
Mock((*Class).FunA).To(mock).Build()
工具函数
·获取私有函数
target := mockito.GetPrivateMethod(rpcClient.FlashServiceClientInst, "GetResourceItems")
mockey.Mock(target).Return(nil, mockErr).Build()
Mock 变量
a := 20
MockValue(&a).To(20)
Mock 结果统计
1. 被mock函数调用次数
API : Times() int
2. hook函数调用次数
API:MockTimes() int
异步方法测试
提供了 IncludeCurrentGoRoutine、ExcludeCurrentGoRoutine 、FilterGoRoutine 关于 Goroutine 限制功能,可以控制Mock的生效 Goroutine 范围。
TestMain
在写测试时,有时需要在测试之前或之后进行额外的设置(setup)或拆卸(teardown)。 为了支持这些需求,testing 提供了 TestMain 函数 :
func TestMain(m * testing . M) {
log . Println("Do stuff BEFORE the tests!")
exitVal := m . Run()
log . Println("Do stuff AFTER the tests!")
os . Exit(exitVal)
}
如果测试文件中包含该函数,那么生成的测试将调用 TestMain(m),而不是直接运行测试。TestMain 运行在主 goroutine 中 , 可以在调用 m.Run 前后做任何设置和拆卸。
Mock Interface
Mockey 是不支持mock interface的
目前提供一种方法 struct实现interface ,然后将变量指定到实现struct的实例,mock stuct方法:
mockDataEngineClient := &data_engine_rpc_mock.DataEngineService{}
data_engine_rpc.DataEngineClient = mockDataEngineClient
mockey.Mock((*data_engine_rpc_mock.DataEngineService).QueryDataInstance).Return(tt.DataInstanceID, nil, nil).Build()
针对interface 生成mock 代码
通过mockgen 工具生成该 Mock 代码, 详细了解请看这里:mockgen使用说明
mockgen -source=<Interface所在的文件名> -destination=<生成的文件名> -package=<包名,建议与Interface的报名一致>
CI集成
增量覆盖率生成
1. 新增配置文件 .codebase/pipelines/ci.yaml
commands:
- go test -gcflags="all=-l -N" -race -v -p 1 -coverprofile=coverage.out ./...
- go tool cover -html=coverage.out -o coverage.html
coverage_report_name: coverage.html
2. 新增配置文件.codebase/apps.yaml
codecov:
status: # 准入控制状态相关
project:
default: # 可以为任何值,代表了当前 paths 下的配置, 支持一个仓库内的多项目配置
coverage_strategy: statement # branch or statement, default statement. not support diff coverage. Supported languages: node and c++
minimum_coverage: 80% # 允许的覆盖率的最小值
threshold: 0% # 允许少于目标值的范围 base: "change" # auto or parent or change。 change: 与目标分支覆盖率进行比较,parent:与父 commit 进行比较, 与 minimum_coverage 二选一
line_limit: 10 # 增量行数少于多少行时,默认置成功
paths: # 支持根据不同的路径计算覆盖率。不添加该项时默认为全部路径
- "project/src" # 匹配以 project/src 开头的路径
- "!test" # 匹配所有不以 test 开头的路径
- "!a/*.go" # 过滤所有 a 文件夹内的 go 文件
project_test: #与上面的 default 为同一级
paths:
- "project/test"
diff: # 具体下级配置同上面的 project, 只对 diff 的代码有效
数据库(Mysql)
template: go
enable_mysql: true
mysql_database: my_db
mysql_password: my_password
mysql_image: hub.byted.org/ee/mysql:8.0.25
mysql_script_paths:
- "resources/db/*.sql"
- "resources/table/*.sql"
- "resources/data/*.sql"
其他场景
如何在老代码下写单测
难点
·可测性低
· 业务场景复杂、历史功能耦合
· 构造数据难,相比于原子性的方法而言
例子
步骤一: 梳理功能
步骤二:场景
正常场景
异常场景
步骤三:单测函数拆分结构
根
功能性拆分
实现能力拆分
前置依赖拆分
步骤四:构造单测case
结合场景和函数拆分构造case完成360覆盖:
复杂场景数据构造
接口拦截json 打印,或者从页面请求中获取。
本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理