字节golang单元测试框架实践-SEO版本 | 客服服务营销数智化洞察_晓观点
       

字节golang单元测试框架实践-SEO版本

一、单测必要性

单元测试不仅是确保代码正确性的重要手段,也是提升开发效率、改善代码设计、降低维护成本的重要工具。通过系统化的单元测试,团队能够构建高质量、可维护、易扩展的软件系统,满足业务需求和用户期望。因此,单元测试在现代软件开发中具有不可替代的必要性。

尤其是核心链路、核心业务。尤其是需要反复回归测试的逻辑单元。以下是单元测试的重要性详细解析:

1. 提高代码质量

单元测试通过对代码的各个独立模块进行测试,确保每个模块按照预期工作。这有助于发现和修复代码中的缺陷,从而提高整体代码质量。

2. 早期发现错误

在开发的早期阶段进行单元测试,可以及早发现并修复错误,减少后期修复成本。早期的错误修复通常比后期更高效、更经济。

3. 促进代码重构

有了完善的单元测试,开发人员可以自信地对代码进行重构。单测确保了重构过程中功能的正确性,不会因为修改代码结构而引入新的错误。

4. 提供持续集成的基础

单元测试是持续集成(CI)和持续部署(CD)流程的重要组成部分。自动化的单测能够在每次代码提交时即时验证代码的正确性,确保集成的稳定性。

5. 改善设计和模块化

编写单元测试通常要求代码具有良好的可测试性,这促使开发人员设计出更加模块化、低耦合、高内聚的代码结构,提高代码的可维护性和可扩展性。

6. 文档化代码行为

单元测试可以作为代码行为的活文档,通过测试用例可以清晰地了解每个模块的功能和预期结果,对于新成员了解代码和系统功能具有重要参考价值。

7. 降低维护成本

有了完善的单元测试,维护和更新代码变得更加可靠和高效。单测帮助确保现有功能不被新的更改破坏,从而减少了因代码变更导致的回归问题。

8. 增强开发信心

开发人员在编写单元测试后,对自己的代码质量有更高的信心。这种信心能够提高开发效率,减少因不确定性带来的焦虑。

二、单测不好做的原因

  1. 依赖过多,且不知道怎么模拟调用
    1. 模块、函数中调用其他模块、函数, 比如请求一个远程调用、访问了redis缓存等等
  2. 增加工作量?
    1. 善用Code copilot
  3. 模块逻辑可测试性不好:分层不合理、逻辑拆分不合理、逻辑混乱
    1. (模块、函数)遵循职责单一原则。有好的模块划分,才有好的可测试性。
字节golang单元测试框架实践-SEO版本
  1. 本次修改的地方,我不是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

(为避免纸上谈兵)下面通过 实现某真实业务场景里面一段代码的单元测试展示怎么使用。

  1. 先看原始代码片段:

逻辑描述 – 先获取店铺信息,然后根据店铺模型类型决定 去调用万花筒模型、还是以前老的模型 做语义识别。然后再进一步获取该类目自动发送阈值 和 命中的行业场景详情,组装后返回。

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
}
  1. 单元测试代码
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")
                })
        })
}
  1. 上面涉及到的 抽象接口是怎么 mock的

示例:

字节golang单元测试框架实践-SEO版本
go generate ./...

生成mock代码 mock_api.go

  1. 执行结果
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)
  1. 重要 : 关闭编译优化参数
go test -gcflags="all=-l -N" -v ./...
免费试用 更多热门智能应用                        
(0)
研发专家-云白研发专家-云白
上一篇 2025年1月4日
下一篇 2024年9月18日

相关推荐