怎么样在Golang里面写UT
UT test是测试中的最小测试单元,一般而言,需要fake 输入,compare预期输出,最理想或者最简单的情况是输入特别易于fake,看下面的例子
建设我们有一个实现➕1功能的函数 sum.go
// sum.go
package sum
func addOne(a int) int {
return a + 1
}
如果你要为这个函数写UT,你需要创建一个测试文件 sum_test.go
,这样当你运行命令 go test xxx
时, go 会自动运行package下面的所有的 *_test.go
// sum_test.go
package sum
import (
"errors"
"testing"
)
func TestAddOne(t *testing.T) {// function name must bu Upper case, or it will not be run
if addOne(3) != 4 {
t.Errorf("the expect result should be 4, but got %v", addOne(3))
return
}
}
UT的难点在于input的fake,在真实项目中,很少有函数的输入会上上例中的一样简单,在大一点的项目中,肯定会用到函数的组装,流程控制,外部包,系统函数的引用等等,对于复杂输入的情况,怎么样写UT呢?
流程控制函数的mock
我们看一个例子
// car.go
package car
type Car struct {
}
func NewCar() *Car {
return &Car{}
}
func (c *Car) start() error {
return nil
}
func (c *Car) stop() error {
return nil
}
func run(car *Car) error {
if err := car.assemble(); err != nil {
return err
}
if err := car.start(); err != nil {
return err
}
if err := car.stop(); err != nil {
return err
}
}
func main() {
car := NewCar()
run(car)
}
在上例中, run
组合了三个函数,通常UT的做法就是让 run
函数接收一个 interface类型的参数,而不是一种具体的实现类型,让我们按照这个思路改动下
type RunBehavior interface {
start() error
stop() error
}
func run(r RunBehavior) {
if err := r.start(); err != nil {
return err
}
if err := r.stop(); err != nil {
return err
}
}
func main() {
car := NewCar()
run(car)
}
经过改动之后, run
的输入参数是一个接口 RunBehavior
, 然后我们看怎么样对 run
进行UT
// car_test.go
package car
import "fmt"
type testCar {
startErr error
stopErr error
}
func (c *Car) start() error {
return c.startErr
}
func (c *Car) stop() error {
return c.stopErr
}
func TestRun(t *testing.T) {
// start error
car := &testCar{startErr: fmt.Errorf("start error, exit")}
if err := run(car); err == nil || err.Error() != "start error, exit" {
t.Errorf("xxx")
return
}
// start error
car := &testCar{stopErr: fmt.Errorf("stop error, exit")}
if err := run(car); err == nil || err.Error() != "stop error, exit" {
t.Errorf("xxx")
return
}
// ok
car := &testCar{}
if err := run(car); err != nil {
t.Errorf("xxx")
return
}
}
从上例子中能看到,我们创建了一个 testCar
的实现,然后对 run
进行测试的时候,就可以很容易的 run 函数中的每一步流程进行控制,你可以得到任意输想要的每一步返回结果
系统调用的mock
// read.go
import "os"
func read(path string) error {
f, err := os.Open("notes.txt")
if err != nil {
return err
}
return nil
}
如果要对 read
函数做测试,并没有太好的办法,通常有两种做法,一种是 把 os.OpenFile
做成一个参数
var osOpen = os.Open
func read(path string) error {
f, err := osOpen("notes.txt")
if err != nil {
return err
}
return nil
}
测试的时候定义自己的函数去替换系统函数
oldOsOpen := osOpen
defer func() { osOpen = oldOsOpen }()
myOpen := func(path string)(*os.File, error) {
return nil, nil
}
osOpen = myOpen
这种做法是hack的做法,比较出错,需要用到全局变量,但是确实可以把一些不能测到的函数变得可测试,当然也可以使用接口的方法,让 read
函数接收一个 io.Reader
interface, 这种方法就好得多。
函数作为参数
还有一种比较改进的做法是把函数作为参数放到参数里面
type OpenFunc func(string) (*File, error)
func read(fn OpenFunc, path string) error {
f, err := fn(path)
if err != nil {
return err
}
return nil
}
通过把函数作为参数,也可以很容易的mock掉函数调用函数的情况,但是这种做法适用于函数内调用函数的数量比较少,否则你就需要传递大量的函数参数作为输入,这样代码写出来也很丑陋。
小结
golang原生UT的方法就是尽量使用接口作为参数,尽量不引入比较heavy的测试框架, 但是 sqlmock
此类查询调用的没有太好的办法,还是乖乖的使用第三包比较省心。golang的UT确实没有python的好写,python支持的各种高级mock,可以让你任意替换掉函数内的任意函数的返回值,虽然这样易于UT,但是这也是双刃剑。像golang这样的UT策略,对代码实现的组织和设计要求比较高,否则代码根本就没办法UT🤣。