关于 Golang 单元测试的一点实践
为什么需要单元测试?
在没去公司之前,我一般没怎么写过单元测试,debug 代码时,直接 fmt.Println 打印即可解决,再复杂一点的话,使用 dlv 去 debug 代码,找出问题。
但是在工作中无法避免遇到一些问题:
- 没办法在电脑上完整运行整个项目,无法看到效果
- 写完代码,没办法部署,不知道自己写的逻辑对不对
- 自己写的代码中会用到一些其他人写的接口,直接调用不了
- 代码覆盖率太低,导致代码提交不了等问题
那这个时候,单元测试就非常重要了。
单元测试简单实践
下面,我们就模拟一个场景来讲解单元测试的使用。
一般的,我们称能完整运行代码,即有 main 函数的入口,但我们工作的话,一般都是写一些类似 package 的包,那么,就导致无法运行 main 函数。
解决方案即写单元测试,假设我们现在写一个叫做 add 的 package.
首先我们使用 go mod init mytest
初始化目录,使用 gomodule 管理依赖。
然后创建一个 add 的目录,mkdir add
.
进入 add 目录,cd add
.
创建一个文件,touch add.go
创建一个单元测试文件,touch add_test.go
此时,目录结构为:
.
├── add
│ ├── add.go
│ └── add_test.go
└── go.mod
1 directory, 3 files
我们的工作就是实现 add 包,能够实现两个数相加并返回结果的一个接口。
在 add.go
中写入如下内容:
package add
func Add(a, b int) int {
return a + b + 1
}
那么我们如何测试该段代码逻辑是不是正确的呢?
在add_test.go
中写入如下内容:
这里我用到了 github/stretchr/testify/assert
直接使用 go get -u github.com/stretchr/testify/assert
安装即可。
package add
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAdd(t *testing.T) {
// 实际通过该接口得到的值
actual := Add(1, 1)
// 我们期望得到的值
expected := 2
// 是否符合我们的预期
assert.Equal(t, expected, actual)
}
代码中,actual
为通过Add
接口得到的值,expected
为我们期望得到的值,assert.Equal
即判断是否符合期望,如果不符合,会有相关信息提示。
写完上述内容之后,我们就可以进行单测了,可以使用 ide 的一键运行,也可以使用命令行进行单测,这里我们使用命令行。
go test -v .
很明显,我们期望的值是 2,而不是3,所以报错了。经过排查确实代码逻辑存在问题:
return a + b + 1
将这行代码改成即可:
return a + b
可以看到测试通过。
工作中的单元测试实践
当然单测不可能像上面写的那么的简单~
因为中间还有很多的复杂逻辑,以及调用的一系列接口,这个时候就需要借助其他的工具了,多说无益,看代码。
将add.go
替换为如下的内容:
package add
import (
"encoding/json"
"fmt"
)
type AddTwo struct {
A int `json: "a"`
B int `json: "b"`
}
func Add(args []byte) int {
at, err := parseArgs(args)
if err != nil {
fmt.Println(err)
return -1
}
return at.A + at.B + 1
}
func parseArgs(args []byte) (*AddTwo, error) {
// http 请求或者请求了其他包中的函数等
var at AddTwo
err := json.Unmarshal(args, &at)
if err != nil {
return nil, err
}
return &at, nil
}
现在,Add
接口中的值,我们需要通过函数 parseArgs
去解析,但是由于某种因素我们无法直接去调用该接口。
因此我们需要通过某种手段保证我们调用的这个接口能返回我们想要的值,我们并不在乎别人写的接口是否正确,只要保证我们自己的逻辑是对的就行。
这就需要介绍一个新的东西 mock
,我是这样理解的,我不关心别人接口逻辑是否正确,那是别人的工作,与我无关。我只要按照文档照着别人返回的值模拟一个值出来即可。
首先,go get -u github.com/agiledragon/gomonkey
将 add_test.go
替换为如下的内容:
package add
import (
"testing"
"github.com/agiledragon/gomonkey"
"github.com/stretchr/testify/assert"
)
func TestAdd(t *testing.T) {
mocks := func(t *testing.T) *gomonkey.Patches {
patches := gomonkey.NewPatches()
patches.ApplyFunc(parseArgs, func([]byte) (*AddTwo, error) {
t.Log("mock parseArgs")
// return 我们需要的值
return &AddTwo{A: 1, B: 2}, nil
})
return patches
}
t.Run("test function add", func(t *testing.T) {
patches := mocks(t)
defer patches.Reset()
args := `{"a":1, "b":2}`
actual := Add([]byte(args))
expected := 3
assert.Equal(t, expected, actual)
})
}
上述代码中,按照原本的逻辑,传入参数 args
后需要经过 parseArgs
得到返回值,再计算两数之和,但是因为某种因素我们无法正常调用该函数。
通过 patcher.ApplyFunc
将 parseArgs
的返回值设置成了我们想要的数据,避免其通过调用函数 parseArgs
返回值,保证了这段测试代码中只测试了我们自己的代码逻辑并能正确运行这段代码。
我们传进来的值为 {"a":1, "b":2}
,经过我们自己的 mock,得到的返回值为:
&AddTwo{
A: 1,
B, 2,
}
再经过我们自己的逻辑,得到返回值。从测试代码来看,正常得到的值应该为3,但是单测之后发现:
所以通过这段单测代码可以判断代码逻辑是由问题的,果然经过排查后发现:
return at.A + at.B + 1
这里应该是:
return at.A + at.B
以上就是单测的一些实践,当然真实情况还比上面写到的复杂一点,具体还需要自己去实践的哈。