注:本文由linstring原创,博客地址:https://blog.youkuaiyun.com/lin_strong
转载请注明出处
第一部分链接:
https://blog.youkuaiyun.com/lin_strong/article/details/109012560
文章目录
Verify
原生
*testing.T
提供了方法来管理测试状态和辅助打印日志等。
Fail
方法用于标记测试用例失败,其他类似方法都是其封装;
func (c *T) Fail()
标记为失败的测试不会立即结束,这样就可以继续完成其他的Check,如果需要立即结束任务的话:
func (c *T) FailNow()
FailNow
会标记测试失败并立即退出测试。
FailNow
通过调用runtime结束当前goroutine的方式实现退出测试,因此必须在测试所在goroutine调用。并且因为结束的是goroutine,不会停止其他测试。
Log
和Logf
方法则用于打印信息到测试的记录日志中去,分别对应fmt.Println
和fmt.Printf
,其他类似方法都是其封装;错误日志只有在要求打印,或者测试失败时会真正输出。
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
Error = Log + Fail
Errorf = Logf + Fail
Fatal = Log + FailNow
Fatalf = Logf + FailNow
Go语言原生不提供其他测试框架常见的assert,因为官方认为这可能导致用户懒于思考良好的错误处理和汇报:
https://golang.org/doc/faq#assertions
第三方包testify中提供了assert和require:
https://github.com/stretchr/testify
assert
assert
package可以:
- 打印友好,易读的失败描述
- 有助于写可读性良好的代码
- 可选地为每个断言做注释一条消息
assert提供了很多方便的Helper函数,底层使用的是Fail系,所以失败时不会导致退出测试
常用的如
- assert.Equal
- assert.NotEqual
- assert.Zero
- assert.NotZero
- assert.Nil
- assert.NotNil
还有如:
- assert.Implements
- assert.Contains
- assert.InDelta
之类的很多很方便的函数。一些示例:
package testifyassert
import (
"errors"
"github.com/stretchr/testify/assert"
"testing"
)
func TestAssertEqual(t *testing.T) {
expect := 3
actual := 2
assert.Equal(t, expect, actual)
actual = 3
assert.Equal(t, expect, actual)
assert.Equal(t, int32(1), int64(1))
}
func TestAssertInDelta(t *testing.T) {
expect := 0.5
actual := 0.47
assert.InDelta(t, expect, actual, 0.02)
assert.InDelta(t, expect, actual, 0.04)
}
func ReturnErr() error{
return errors.New("test")
}
func TestAssertNil(t *testing.T) {
assert.NotNil(t, ReturnErr())
assert.Nil(t, ReturnErr())
}
func TestAssertContain(t *testing.T){
assert.Contains(t, []rune{'A', 'B'}, 'C')
assert.NotContains(t, []rune{'A', 'B'}, 'C')
}
输出如下:
$ go test -v
=== RUN TestAssertEqual
--- FAIL: TestAssertEqual (0.00s)
demo_test.go:12:
Error Trace: demo_test.go:12
Error: Not equal:
expected: 3
actual : 2
Test: TestAssertEqual
demo_test.go:15:
Error Trace: demo_test.go:15
Error: Not equal:
expected: int32(1)
actual : int64(1)
Test: TestAssertEqual
=== RUN TestAssertInDelta
--- FAIL: TestAssertInDelta (0.00s)
demo_test.go:21:
Error Trace: demo_test.go:21
Error: Max difference between 0.5 and 0.47 allowed is 0.02, but difference was 0.030000000000000027
Test: TestAssertInDelta
=== RUN TestAssertNil
--- FAIL: TestAssertNil (0.00s)
demo_test.go:31:
Error Trace: demo_test.go:31
Error: Expected nil, but got: &errors.errorString{s:"test"}
Test: TestAssertNil
=== RUN TestAssertContain
--- FAIL: TestAssertContain (0.00s)
demo_test.go:35:
Error Trace: demo_test.go:35
Error: []int32{65, 66} does not contain 67
Test: TestAssertContain
FAIL
exit status 1
FAIL go_test_demo/testifyassert 0.830s
注意:equal方法中,int32(1)和int64(1)是不相等的,因为他们类型不一样。要注意转换类型使两个被比较对象类型一致。
require
require
和assert
的方法完全一致,调用上只需要直接替换名字即可。但是底层使用的是Fatal系,会直接终止测试。
import "github.com/stretchr/testify/require"
func TestRequire(t *testing.T) {
require.Equal(t, 0, 1)
assert.Equal(t, 0, 1)
}
由于require断言失败,后面的断言直接跳过没有执行。
$ go test -run=Requi
--- FAIL: TestRequire (0.00s)
demo_test.go:41:
Error Trace: demo_test.go:41
Error: Not equal:
expected: 0
actual : 1
Test: TestRequire
FAIL
exit status 1
FAIL go_test_demo/testifyassert 1.063s
测试替身
自包含的模块很好测试,但我们经常要测试一些中间的代码,它们依赖于很多其他的模块。而这些其他的模块可能难以预测、有很大延时、不好控制返回值,比如像rpc调用、数据库、特定硬件这样的。我们需要方法掌控它们,这时就可以选择使用测试替身替换掉真实的模块。
测试替身概念
测试替身(test double)在测试中扮演一些函数、数据、模块或库。CUT(Code Under Test)不知道使用的是替身;它按与协作者同样的方式与替身交互。
测试替身并不是完全模拟它替代的那个东西,这使得替身能简单很多。
测试替身可以给CUT提供间接的输入(返回值)或以捕捉的方式,并且能够检查CUT发送给测试替身的间接输出(参数)。
测试替身最简单的形式就是替代真实产品代码的一个测试桩(stub)。
测试替身的类型
Gerard Meszaros的书xUnit Testing Patterns中定义了多种测试替身。
- 测试傀儡(Test dummy):只是为了链接器不报警而创建的测试替身。dummy是一个永远不会被调用的桩。它是用来满足编译器、链接器或运行时依赖的。
- 测试桩(Test stub):按照当前测试用例的指示返回一些值。
- 测试间谍(Test spy):捕获从CUT传来的参数,这样测试用例就可以验证正常的参数被传给了CUT。spy也可以像测试桩一样喂给CUT返回值。
- Mock对象:验证被调用的函数,调用顺序以及从CUT传给DOC的参数。同样被编程为返回特定的值给CUT。mock对象常用于处理那种需要多次调用并且每次的调用和响应可能不同的情况。
- Fake对象:提供被替换组件的部分实现。fake的实现通常相对于被替换的那个会简单的多。
- Exploding fake:如果被调用会导致测试失败。
当讨论测试替身需要的不同行为和能力时,这些名词很有用。但是通常在实践中我们并不需要特意区分它们。你会发现人们常常随意地使用fake、mock和stub。
使用测试替身的时机
不是总无脑用测试替身。能用真的代码时就不要用替身。需要自己判断要不要欺骗CUT。
使用测试替身的一些常见原因:
- 硬件解耦:这样就不需要硬件就能测试了,还能提供难以实现的多种输入。
- 注入难以产生的输入:通过调整测试替身的返回值,以测试难以测试到的执行路径。
- 加速一个慢速collaborator:如数据库、网络服务等。
- 依赖于一些易变的东西:如时钟。
- 依赖于开发中的东西:这时使用替身使得测试得以继续,同时有助于发现CUT需要,但当前尚未实现的服务需求。
- 依赖于一些难以配置的东西:如果DOC难以配置为想要的状态,如数据库。
一个最简单的测试替身
在一些特别简单的情况下,可以直接手写测试替身,如对这样一个特别简单的函数:
package simplestub
func CallTheFuncAndReturnValue(cb func() int) int{
return cb()
}
其测试可以是这样的:
package simplestub
import (
"github.com/stretchr/testify/assert"
"testing"
)
var called = false
func callback() int {
called = true
return 10
}
func TestCallTheFunc(t *testing.T) {
got := CallTheFuncAndReturnValue(callback)
assert.Equal(t, 10, got)
assert.True(t, called)
}
Mocking Frameworks
GoMock和Testify是Go的两大Mocking框架。都可以为Go生成Mock对象以辅助测试,他们两的详细比较如下:
GoMock vs. Testify: Mocking frameworks for Go
深入介绍二者对本篇来说可能过长了。或许自己去深入下?
GoMock
https://github.com/golang/mock
https://godoc.org/github.com/golang/mock/gomock#example-Call-Do-CaptureArguments
Testify
https://github.com/stretchr/testify
Mock Code for Test
为了让代码可测试,代码应该尽可能依赖于抽象,而不是直接依赖于实际的模块。
Andrew Gerrand:Go避免mock和fake,而喜欢编写具有广泛接口的代码。
https://talks.golang.org/2014/testing.slide#22
假设在写一个灯的开启策略,不考虑时间延时问题,只是按照一定顺序打开灯。
直觉上大概这样写:
var ctrler = new(ledboard.LedBoard)
func init() {
ctrler.PowerUp()
}
func TurnOnSequentially(){
cnt := ctrler.Count()
for i := 0; i < cnt; i++ {
ctrler.Open(i)
}
}
但这样直接依赖于LedBoard
这个结构体,难以替换和测试,要是以后不是Led灯了呢?而且为什么一定要是灯?
依赖于接口
更好的做法可能是TurnOnSequentially
只依赖于接口,定义接口并传入,其只负责控制接口:
package controller
type Controller interface {
PowerUp() error
Open(num int) error
Close(num int) error
Count() int
PowerDown() error
}
package ctrlservice
import "go_test_demo/controller"
func TurnOnSequentially(ctrler controller.Controller){
cnt := ctrler.Count()
for i := 0; i < cnt; i++ {
ctrler.Open(i)
}
}
这样,我们不关心控制器的具体实现,只依赖于这个接口,同时,也方便了我们监控其行为。
创建实现接口的mock对象
下面我们以官方的Mock库—GoMock为例看看怎么为其写测试。
先进入要mock的接口的同目录下,然后使用mockgen工具:
$ mockgen -source=controller.go -destination=mock/Controller.go
即可在destination选项指定的文件得到生成的mock对象,否则默认输出到标准输出:
// Code generated by MockGen. DO NOT EDIT.
// Source: controller.go
// Package mock_controller is a generated GoMock package.
package mock_controller
import (
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockController is a mock of Controller interface
type MockController struct {
ctrl *gomock.Controller
recorder *MockControllerMockRecorder
}
// MockControllerMockRecorder is the mock recorder for MockController
type MockControllerMockRecorder struct {
mock *MockController
}
// NewMockController creates a new mock instance
func NewMockController(ctrl *gomock.Controller) *MockController {
mock := &MockController{ctrl: ctrl}
mock.recorder = &MockControllerMockRecorder{mock}
return mock
}
………………………………
这个mock对象实现了Controller
接口。
Setup exception
接下来,为TurnOnSequentially
写测试。
接下来简单介绍下在基于gomock的测试中要怎么为mock对象设置期望。
GoMock的所有Mock对象捆绑于一个测试控制器一同设置和验证,需要为每个(子)测试创建一个新TestController,在测试结束时调用Finish来触发Verfiy
ctrl := gomock.NewController(t)
defer ctrl.Finish()
然后New每个mock对象出来,并绑定此测试控制器
mockCtrler := mock_controller.NewMockController(ctrl)
有了mock对象后,就可以通过EXCEPT来设置你期望mock对象会被调用某个函数,应该传递的是什么参数,应该return什么值等。比如我期望至少count方法会被调用一次,当调用时应该返回3:
mockCtrler.EXPECT().Count().Return(3).MinTimes(1)
甚至可以通过gomock.InOrder指定期望的调用顺序。
Test case
这样,我们结合gomock后写出的测试就成了这个样子:
package ctrlservice
import (
"github.com/golang/mock/gomock"
mock_controller "go_test_demo/controller/mock"
"testing"
)
func TestTurnOnSequentially(t *testing.T) {
// setup
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockCtrler := mock_controller.NewMockController(ctrl)
mockCtrler.EXPECT().Count().Return(3).AnyTimes()
gomock.InOrder(
mockCtrler.EXPECT().Open(0).Return(nil),
mockCtrler.EXPECT().Open(1).Return(nil),
mockCtrler.EXPECT().Open(2).Return(nil),
)
// exercise
TurnOnSequentially(mockCtrler)
// and verify (with defer)
}
这样,就可以验证
TurnOnSequentially
起码会调用接口的Count方法一次(考虑到多次调用以检查count也并不算错),并且我们会给其返回一个3。TurnOnSequentially
会调用Open方法三次,并且是以传参0、1、2的顺序调用的,每次给其返回nil,表示没有出错
当然,也可以进行更精细的exception控制,以保证先会调用一次Count后才调用Open,并允许后面调用任意次Count(如果真觉得有必要控制的这么精细的话):
……
cl := mockCtrler.EXPECT().Count().Return(3)
gomock.InOrder(
cl,
mockCtrler.EXPECT().Open(0).Return(nil),
mockCtrler.EXPECT().Open(1).Return(nil),
mockCtrler.EXPECT().Open(2).Return(nil),
)
mockCtrler.EXPECT().Count().Return(3).AnyTimes().After(cl)
……
Table-driven with mock
但其实这一个测试还远远不够测试这个函数:
- 其实是很容易绕过这么简单的单个测试的,比如这么写:
func TurnOnSequentially(ctrler controller.Controller){
_ = ctrler.Count()
for i := 0; i < 3; i++ {
ctrler.Open(i)
}
}
- 还有很多边界条件要处理,比如nil处理,Open Err时的处理等
想就知道适合写成表驱动的样子。GoLand自动生成的代码是这样的:
func TestTurnOnSequentially(t *testing.T) {
type args struct {
ctrler controller.Controller
}
tests := []struct {
name string
args args
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
})
}
}
在带上mock后其实这并不是很合适的表,需要进行一定的改造,实践中比如可以给一个setup函数,并返回一个需要defer的函数。供参考:
package ctrlservicetd
import (
"errors"
"github.com/golang/mock/gomock"
"go_test_demo/controller"
mock_controller "go_test_demo/controller/mock"
"testing"
)
func TestTurnOnSequentially(t *testing.T) {
tests := []struct {
name string
setUp func(t *testing.T) (ctrler controller.Controller, deferFn func())
}{
{"Nil", func(t *testing.T) (ctrler controller.Controller, deferFn func()) {
return nil, nil
}},
{"AnyErrorToStop", func(t *testing.T) (ctrler controller.Controller, deferFn func()) {
ctrl := gomock.NewController(t)
mockCtrler := mock_controller.NewMockController(ctrl)
mockCtrler.EXPECT().Count().Return(3).AnyTimes()
gomock.InOrder(
mockCtrler.EXPECT().Open(0).Return(nil),
mockCtrler.EXPECT().Open(1).Return(errors.New("test")),
)
return mockCtrler, func() { ctrl.Finish() }
}},
{"Count0DoNothing", func(t *testing.T) (ctrler controller.Controller, deferFn func()) {
ctrl := gomock.NewController(t)
mockCtrler := mock_controller.NewMockController(ctrl)
mockCtrler.EXPECT().Count().Return(0)
return mockCtrler, func() { ctrl.Finish() }
}},
{"CountNegativeDoNothing", func(t *testing.T) (ctrler controller.Controller, deferFn func()) {
ctrl := gomock.NewController(t)
mockCtrler := mock_controller.NewMockController(ctrl)
mockCtrler.EXPECT().Count().Return(-1)
return mockCtrler, func() { ctrl.Finish() }
}},
{"Success", func(t *testing.T) (ctrler controller.Controller, deferFn func()) {
ctrl := gomock.NewController(t)
mockCtrler := mock_controller.NewMockController(ctrl)
mockCtrler.EXPECT().Count().Return(3).AnyTimes()
gomock.InOrder(
mockCtrler.EXPECT().Open(0).Return(nil),
mockCtrler.EXPECT().Open(1).Return(nil),
mockCtrler.EXPECT().Open(2).Return(nil),
)
return mockCtrler, func() { ctrl.Finish() }
}},
{"Success", func(t *testing.T) (ctrler controller.Controller, deferFn func()) {
ctrl := gomock.NewController(t)
mockCtrler := mock_controller.NewMockController(ctrl)
mockCtrler.EXPECT().Count().Return(5).AnyTimes()
gomock.InOrder(
mockCtrler.EXPECT().Open(0).Return(nil),
mockCtrler.EXPECT().Open(1).Return(nil),
mockCtrler.EXPECT().Open(2).Return(nil),
mockCtrler.EXPECT().Open(3).Return(nil),
mockCtrler.EXPECT().Open(4).Return(nil),
)
return mockCtrler, func() { ctrl.Finish() }
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrler, deferFn := tt.setUp(t)
if deferFn != nil {
defer deferFn()
}
TurnOnSequentially(ctrler)
})
}
}
一个能通过测试的TurnOnSequentially
:
package ctrlservicetd
import (
"fmt"
"go_test_demo/controller"
)
func TurnOnSequentially(ctrler controller.Controller) {
if ctrler == nil {
return
}
cnt := ctrler.Count()
if cnt < 0 {
fmt.Printf("Warn: [TurnOnSequentially] Negative count: %v\n", cnt)
}
for i := 0; i < cnt; i++ {
err := ctrler.Open(i)
if err != nil {
fmt.Printf("Error: [TurnOnSequentially] Fail on %v/%v, err: %v\n", i, cnt, err)
return
}
}
}
测试执行结果:
$ go test -v
=== RUN TestTurnOnSequentially
=== RUN TestTurnOnSequentially/Nil
=== RUN TestTurnOnSequentially/AnyErrorToStop
Error: [TurnOnSequentially] Fail on 1/3, err: test
=== RUN TestTurnOnSequentially/Count0DoNothing
=== RUN TestTurnOnSequentially/CountNegativeDoNothing
Warn: [TurnOnSequentially] Negative count: -1
=== RUN TestTurnOnSequentially/Success
=== RUN TestTurnOnSequentially/Success#01
--- PASS: TestTurnOnSequentially (0.00s)
--- PASS: TestTurnOnSequentially/Nil (0.00s)
--- PASS: TestTurnOnSequentially/AnyErrorToStop (0.00s)
--- PASS: TestTurnOnSequentially/Count0DoNothing (0.00s)
--- PASS: TestTurnOnSequentially/CountNegativeDoNothing (0.00s)
--- PASS: TestTurnOnSequentially/Success (0.00s)
--- PASS: TestTurnOnSequentially/Success#01 (0.00s)
PASS
ok go_test_demo/ctrlservicetd 0.839s
以及100%的覆盖率: