背景:
What 什么是单元测试?
Why 为什么要写单元测试?
Who 谁来写单元测试?
How 怎么写单元测试?
最近组里一些新同学写代码不写单测,好几次提测差点被QA打回,让人很是惆怅,一问才知他们根本不知道怎样写单测。于是我寻思着整理整理,试图理出一份通俗易懂的写单测文档,当作是自己的笔记也希望能够帮助到大家。
什么是单元测试
单元测试又叫模块测试,是针对程序设计最小单元进行正确性校验的测试工作。
这里的最小单元通常是一个程序模块,在代码实现上则是一个函数,单元测试只需要关注函数内部实现逻辑。
为什么要写单元测试
单元测试是所有测试中最底层的一类测试,例如软件测试中的V模型:
它是软件测试第一个环节,也是最重要的一个环节,是唯一一个能够保证代码覆盖率达到100%的测试环节,是需求提测的基础和前提,也是避免提测被打回的利器。
多说一句:你对自己写的代码真的有十足信心,认为一定符合你设计的初衷不出现任何Bug吗?
谁来写单元测试
20%
怎么写好单元测试
首先 一个好的单元测试应该具备以下的特点:
- 简洁 清晰 易读
- 可维护 它能够让他人通过单测一眼明白该如何调用这个函数,以及能达到什么样的效果(返回什么样的参数)
以后可能会有其他人继续修改该方法以实现新的需求,而一个好的单测应该是方便他人拓展并进行增量测试的,至少应该让别人知道该如何修改并让单测跑起来。
一个单测一个断言?
这是一个很好的规范,但我觉得不应该强制,只要能保持单测简洁清晰,并且达到测试的目的即可,而且很多复杂的函数也不适合用断言。
Golang 单元测试
常见golang单元测试框架:
名称 | 优缺点 |
---|---|
Testing | 官方自带,不支持断言和mock |
Gocheck | 基于Testing,自持断言和mock |
Testify | 和Gocheck相似,也是基于Testing实现,易用性好,但功能更为强大,自带mock工具,支持suite,可以进行用例管理 |
我们的项目中选用的就是Testify。
抛砖引玉
写一个求和的函数以及单测:
测试文件以 _test.go结束,测试的方法则以Testxxx 开始,构造所有可能的参数,覆盖到所有代码逻辑逻辑
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也打印出来。
对于需要依赖实例的函数,比如与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语言如何写出单测
从头到脚说单测——谈有效的单元测试
《代码整洁之道》