背景:
What 什么是单元测试?
Why 为什么要写单元测试?
Who 谁来写单元测试?
How 怎么写单元测试?

最近组里一些新同学写代码不写单测,好几次提测差点被QA打回,让人很是惆怅,一问才知他们根本不知道怎样写单测。于是我寻思着整理整理,试图理出一份通俗易懂的写单测文档,当作是自己的笔记也希望能够帮助到大家。

什么是单元测试

单元测试又叫模块测试,是针对程序设计最小单元进行正确性校验的测试工作。
这里的最小单元通常是一个程序模块,在代码实现上则是一个函数,单元测试只需要关注函数内部实现逻辑。

为什么要写单元测试

单元测试是所有测试中最底层的一类测试,例如软件测试中的V模型:

image.png


它是软件测试第一个环节,也是最重要的一个环节,是唯一一个能够保证代码覆盖率达到100%的测试环节,是需求提测的基础和前提,也是避免提测被打回的利器。
多说一句:你对自己写的代码真的有十足信心,认为一定符合你设计的初衷不出现任何Bug吗?

谁来写单元测试

20%

怎么写好单元测试

首先 一个好的单元测试应该具备以下的特点:

  • 简洁 清晰 易读
  • 可维护 它能够让他人通过单测一眼明白该如何调用这个函数,以及能达到什么样的效果(返回什么样的参数)

以后可能会有其他人继续修改该方法以实现新的需求,而一个好的单测应该是方便他人拓展并进行增量测试的,至少应该让别人知道该如何修改并让单测跑起来。


一个单测一个断言?
这是一个很好的规范,但我觉得不应该强制,只要能保持单测简洁清晰,并且达到测试的目的即可,而且很多复杂的函数也不适合用断言。

Golang 单元测试

常见golang单元测试框架:

名称优缺点
Testing官方自带,不支持断言和mock
Gocheck基于Testing,自持断言和mock
Testify和Gocheck相似,也是基于Testing实现,易用性好,但功能更为强大,自带mock工具,支持suite,可以进行用例管理

我们的项目中选用的就是Testify。

抛砖引玉
写一个求和的函数以及单测:
测试文件以 _test.go结束,测试的方法则以Testxxx 开始,构造所有可能的参数,覆盖到所有代码逻辑逻辑

截屏2022-01-09 下午11.33.08.png

array_sum.go

func Sum(numbers []int) int {
   sum := 0
   for _, num := range numbers {
      sum += num
   }
   return sum
}
复制代码
array_sum_test.go

func TestSum(t *testing.T)  {
   testCases := []struct {
      input []int
      expect int
      msg string
   }{
      {
         []int{1, 2, 3, 4, 5},
         15,
         "normal-test:",
      },
      {
         []int{1, 2, 3, 4, 5, 0, -5},
         10,
         "with navigate number:",
      },
      {
         []int{-1, -2},
         -3,
         "navigate number sum:",
      },
      {
         []int{-1, -2},
         0,
         "failed-test:",
      },
   }

   for _, tt := range testCases {
      sum := Sum(tt.input)
      msg := fmt.Sprintf("msg:%s input:%v expect:%d result:%d", tt.msg, tt.input, tt.expect, sum)
      if sum != tt.expect {
         t.Errorf("FAILED %s ",msg)
      }else {
         t.Log("PASS", msg)
      }
   }
}
复制代码
testCase
  • input: 调用该方法时的入参
  • expect: 期待的返回值
  • msg: case信息提醒,之所以增加这个msg是因为以后修改这部分逻辑的人 可能不清楚这些参数能达到什么样的效果,所以写了一个简单的msg
input

最终执行go test -v 后可看到:前面三种case都正常返回得到我们期待的结果,最后一个错误的case也打印出来。

截屏2022-01-09 下午11.42.00.png


对于需要依赖实例的函数,比如与DB交互需要先实现一个DB client,则使用suite来进行实例的管理:

type TestRecordDBTestSuite struct {
   suite.Suite
   testRecordDB *TestRecordDB
}

func TestEntireOrderDBTestSuite(t *testing.T) {
   suite.Run(t, new(TestRecordDBTestSuite))
}

func (r *TestRecordDBTestSuite) SetupSuite() {
   mysqlConfig := &mysqlconfig.Mysql{
      Host:     "127.0.0.1",
      Port:     3306,
      User:     "root",
      Password: "",
      Schema:       "lz_test_db",
      DialTimeout:  10000,
      ReadTimeout:  10000,
      WriteTimeout: 10000,
      MaxIdleConns: 50,
      MaxOpenConns: 100,
   }

   mysqlClient := mysqlclient.New(mysqlConfig)
   r.testRecordDB = NewTestRecordDB(mysqlClient)
}

func (r *TestRecordDBTestSuite) Test_CreateRecord()  {
   // ...
}


复制代码
SetupSuite() TestRecordDB

延伸

从简单需求延伸到复杂的需求,单测该如何写?

testcase

总结

看过那部么多书,浏览了那么多的博客,却依旧写不好一个单测。只能总结一个简单的规范,提醒自己。
我以前的leader曾说过一句话: 规范是用来引导愚者,而不是束缚智者。
我是一个愚钝的人,所以力求用简单的规范达到期望的效果。如果您有更好的方式,请不吝赐教。


参考:
go语言如何写出单测
从头到脚说单测——谈有效的单元测试
《代码整洁之道》