关闭

一个完整的单元测试实践(上)

发表于:2024-1-24 09:55

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:佚名    来源:稀土掘金

  单测投入成本以及收益
  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),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号