一、单测必要性
单元测试不仅是确保代码正确性的重要手段,也是提升开发效率、改善代码设计、降低维护成本的重要工具。通过系统化的单元测试,团队能够构建高质量、可维护、易扩展的软件系统,满足业务需求和用户期望。因此,单元测试在现代软件开发中具有不可替代的必要性。
尤其是核心链路、核心业务。尤其是需要反复回归测试的逻辑单元。以下是单元测试的重要性详细解析:
1. 提高代码质量
单元测试通过对代码的各个独立模块进行测试,确保每个模块按照预期工作。这有助于发现和修复代码中的缺陷,从而提高整体代码质量。
2. 早期发现错误
在开发的早期阶段进行单元测试,可以及早发现并修复错误,减少后期修复成本。早期的错误修复通常比后期更高效、更经济。
3. 促进代码重构
有了完善的单元测试,开发人员可以自信地对代码进行重构。单测确保了重构过程中功能的正确性,不会因为修改代码结构而引入新的错误。
4. 提供持续集成的基础
单元测试是持续集成(CI)和持续部署(CD)流程的重要组成部分。自动化的单测能够在每次代码提交时即时验证代码的正确性,确保集成的稳定性。
5. 改善设计和模块化
编写单元测试通常要求代码具有良好的可测试性,这促使开发人员设计出更加模块化、低耦合、高内聚的代码结构,提高代码的可维护性和可扩展性。
6. 文档化代码行为
单元测试可以作为代码行为的活文档,通过测试用例可以清晰地了解每个模块的功能和预期结果,对于新成员了解代码和系统功能具有重要参考价值。
7. 降低维护成本
有了完善的单元测试,维护和更新代码变得更加可靠和高效。单测帮助确保现有功能不被新的更改破坏,从而减少了因代码变更导致的回归问题。
8. 增强开发信心
开发人员在编写单元测试后,对自己的代码质量有更高的信心。这种信心能够提高开发效率,减少因不确定性带来的焦虑。
二、单测不好做的原因
- 依赖过多,且不知道怎么模拟调用
- 模块、函数中调用其他模块、函数, 比如请求一个远程调用、访问了redis缓存等等
- 增加工作量?
- 善用Code copilot
- 模块逻辑可测试性不好:分层不合理、逻辑拆分不合理、逻辑混乱
- (模块、函数)遵循职责单一原则。有好的模块划分,才有好的可测试性。
- 本次修改的地方,我不是owner,没有补单测的动力
有这么多障碍是不是单元测试就真的没法玩了,尤其是历史悠久的项目?
答案是否定的。好消息是任何代码,再乱都没关系,借助适当的测试框架都是可以本地单元测试的。
方法比较多, 我们直接选择字节的套路
三、单测框架 & 工具
1.抽象接口interface{} gomock:
https://github.com/uber-go/mock 代替 https://github.com/golang/mock/
2.打桩工具
a.gomonkey github.com/agiledragon/gomonkey
MAC 上使用问题,可能存在兼容性问题
b.xgo https://github.com/xhd2015/xgo
c.mockey https://github.com/bytedance/mockey
3.测试框架(用例组织)
a.goconvey: 和其他 Stub/Mock 框架的兼容性更好 https://github.com/smartystreets/goconvey/wiki/Documentation
b.Testify
四、字节的选型
gomock + mockey + goconvey
(为避免纸上谈兵)下面通过 实现某真实业务场景里面一段代码的单元测试展示怎么使用。
- 先看原始代码片段:
逻辑描述 – 先获取店铺信息,然后根据店铺模型类型决定 去调用万花筒模型、还是以前老的模型 做语义识别。然后再进一步获取该类目自动发送阈值 和 命中的行业场景详情,组装后返回。
func SemanticAnalyze(ctx context.Context, saReq proto.SemanticAnalyzerReq) (proto.SemanticAnalyzerResp, error) {
var platform = saReq.Platform
var shopID = saReq.ShopID
var question = saReq.Q
shopInfo, err := getShopInfo(ctx, platform, shopID)
if err != nil {
return proto.SemanticAnalyzerResp{}, fmt.Errorf("get shop_info failed: %w", err)
}
var nluResp base_proto.NluResponse
switch shopInfo.ModelType {
case ModelTypeLegacy:
resp, err := intentClassifier(ctx, question, shopInfo.Category)
if err != nil {
return proto.SemanticAnalyzerResp{}, fmt.Errorf("intent_classifier err: %w", err)
}
nluResp = resp
case ModelTypeKldscp:
resp, err := kaleidoscope(ctx, question, shopInfo)
if err != nil {
return proto.SemanticAnalyzerResp{}, fmt.Errorf("kaleidoscope err: %w", err)
}
nluResp = resp
default:
return proto.SemanticAnalyzerResp{}, fmt.Errorf("unknown model type: %d", shopInfo.ModelType)
}
qid := int(nluResp.Intent[0][0])
qprob := nluResp.Intent[0][1]
probThreashold := prob.Get().GetAutoSendThreshold(ctx, platform, shopInfo.Category)
qbInfo, err := getQuestionBInfo(ctx, qid)
if err != nil {
return proto.SemanticAnalyzerResp{}, fmt.Errorf("getQuestionBInfo err: %w", err)
}
resp := proto.SemanticAnalyzerResp{
Sid: nluResp.Sid,
Qid: qid,
Prob: qprob,
ProbThreashold: probThreashold,
Question: qbInfo.QuestionB.Question,
}
return resp, nil
}
- 单元测试代码
func TestSemanticAnalyze(t *testing.T) {
ctrl := gomock.NewController(t)
probAPI := prob.NewMockIAPI(ctrl)
defer ctrl.Finish()
PatchConvey("TestSemanticAnalyze", t, func() {
saReq := proto.SemanticAnalyzerReq{
Q: "This is a Question",
ShopID: "12345",
Platform: "tb",
}
PatchConvey("Case 1: 获取店铺失败,返回获取店铺失败的具体错误信息", func() {
Mock(getShopInfo).Return(shopInfoResp{}, errors.New("get shopInfo failed")).Build()
_, err := SemanticAnalyze(context.Background(), saReq)
So(err, ShouldNotBeNil)
So(err.Error(), ShouldContainSubstring, "get shop_info failed: ")
})
PatchConvey("Case 2: 模型类型测试-不能类型调用不同的识别服务 & 异常返回测试-函数需返回包含识别失败原因的错误信息 ", func() {
PatchConvey("Case 2.1: 模型类型 ModelTypeLegacy,调用老是识别服务", func() {
Mock(getShopInfo).Return(shopInfoResp{
Code: 0,
Category: "category",
ModelType: ModelTypeLegacy,
}, nil).Build()
Mock(intentClassifier).Return(base_proto.NluResponse{}, errors.New("req-intentClassifier-error")).Build()
_, err := SemanticAnalyze(context.Background(), saReq)
So(err.Error(), ShouldContainSubstring, "req-intentClassifier-error")
})
PatchConvey("Case 2.2: 模型类型 ModelTypeKldscp(万花筒),调用万花筒服务", func() {
Mock(getShopInfo).Return(shopInfoResp{
Code: 0,
Category: "category",
ModelType: ModelTypeKldscp,
}, nil).Build()
Mock(kaleidoscope).Return(base_proto.NluResponse{}, errors.New("req-kaleidoscope-error")).Build()
_, err := SemanticAnalyze(context.Background(), saReq)
So(err.Error(), ShouldContainSubstring, "req-kaleidoscope-error")
})
PatchConvey("Case 2.3: 模型类型 其他, 返回未知模型错误", func() {
Mock(getShopInfo).Return(shopInfoResp{
Code: 0,
Category: "category",
ModelType: ModelTypeUnknown,
}, nil).Build()
_, err := SemanticAnalyze(context.Background(), saReq)
So(err.Error(), ShouldContainSubstring, "unknown model type")
})
})
PatchConvey("Case 3: 所有远程调用Ok的情况下, 组装正确的返回", func() {
Mock(getShopInfo).Return(shopInfoResp{
Code: 0,
Category: "category",
ModelType: ModelTypeKldscp,
}, nil).Build()
Mock(kaleidoscope).Return(base_proto.NluResponse{
NluCode: "nlu_code",
Sid: 10010,
Intent: base_proto.IntentSlice{{100010, 0.89}},
}, nil).Build()
Mock(getQuestionBInfo).Return(GetQuestionBInfoResp{
Code: 0,
QuestionB: struct {
QID int `json:"qid"`
SID int `json:"sid"`
Question string `json:"question"`
}{
QID: 100010,
SID: 10010,
Question: "question123",
},
}, nil).Build()
// 用 gomock 生成 prob.IAPI 的 mock 实例
Mock(prob.Get).Return(probAPI).Build()
probAPI.EXPECT().GetAutoSendThreshold(gomock.Any(), gomock.Any(), gomock.Any()).Return(0.5).AnyTimes()
rsp, _ := SemanticAnalyze(context.Background(), saReq)
So(rsp.Prob, ShouldEqual, 0.89)
So(rsp.ProbThreashold, ShouldEqual, 0.5)
So(rsp.Qid, ShouldEqual, 100010)
So(rsp.Sid, ShouldEqual, 10010)
So(rsp.Question, ShouldEqual, "question123")
})
})
}
- 上面涉及到的 抽象接口是怎么 mock的
示例:
go generate ./...
生成mock代码 mock_api.go
- 执行结果
go test -gcflags="all=-l -N" -v ./...
=== RUN TestSemanticAnalyze
TestSemanticAnalyze
Case 1: 获取店铺失败,返回获取店铺失败的具体错误信息 ..
Case 2: 模型类型测试-不能类型调用不同的识别服务 & 异常返回测试-函数需返回包含识别失败原因的错误信息
Case 2.1: 模型类型 ModelTypeLegacy,调用老是识别服务 .
Case 2.2: 模型类型 ModelTypeKldscp(万花筒),调用万花筒服务 .
Case 2.3: 模型类型 其他, 返回未知模型错误 .
Case 3: 所有远程调用Ok的情况下, 组装正确的返回 .....
10 total assertions
--- PASS: TestSemanticAnalyze (0.00s)
PASS
ok gitlab.xiaoduoai.com/ecrobot/robotos/component/semantic (cached)
- 重要 : 关闭编译优化参数
go test -gcflags="all=-l -N" -v ./...
免费试用
更多热门智能应用