要想需求做的好,单测实践少不了。
Posted 泰 戈 尔
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了要想需求做的好,单测实践少不了。相关的知识,希望对你有一定的参考价值。
Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.
文章目录
1 写在前面
打开电脑的备忘录,发现第一条已经安静的躺在那里很久了。
趁着今天休假有点时间,就来整理一下关于单测的相关知识点。
2 痛点分析
2.1 成本层面
日常工作中,提到UT,第一反应就是太浪费时间了吧。单测隶属于白盒测试,因此要对边界值等情况做到很好的覆盖,这就不可避免的会消耗人力成本。在原本排期就紧张的开发阶段,再加入单测的撰写,大部分人都是不乐意的。另外,单测的维护成本也会随着大版本的迭代水涨船高。
2.2 编写层面
单测的粒度从来也没有一个统一的标准,约定俗成并不能保证每个单测的粒度是否合适。单测粒度太小,其也就失去了测试的意义;单测粒度太大,内容包含了好几个测试,会让单测代码本身变得臃肿不堪,难以维护。
3 单测依赖
通常我们遇到的单测依赖类型,大部分为如下几类:
- IO 依赖
- 网络依赖
- 数据库依赖
- 文件依赖
- 函数、方法依赖
4 单测实践
这里主要是以 go 来示例,其实单测的思想是一致的,就不过多赘述了。
4.1 TDT
首先看看最常见的 TDT(Table Driven Tests, 表格式驱动测试)。第一次听到这个词,还是洪岩(sunhongyan@baidu.com)给我说的,着实不赖。TDT 本身没啥特殊的,只不过是官方 testing 的变种写法。下面通过一个例子来看下具体的使用方法。
/**
* scheme 增加透传参数
*/
func paddingExtraParams(scheme string, params map[string]string) string
if scheme == ""
return scheme
paddingLevel := 1
if !strings.Contains(scheme, "?")
scheme += "?"
else
if strings.Contains(scheme, "=")
paddingLevel = 2
if strings.Contains(scheme, "&")
paddingLevel = 2
scheme = strings.TrimRight(scheme, "&")
for key, value := range params
switch paddingLevel
case 1:
scheme += fmt.Sprintf("%s=%s&", key, value)
case 2:
scheme += fmt.Sprintf("&%s=%s", key, value)
scheme = strings.TrimRight(scheme, "&")
return scheme
对应的 TDT 写法就可以这么写。
func Test_paddingExtraParams(t *testing.T)
type args struct
cmd string
params map[string]string
tests := []struct
name string
args args
want string
name: "empty cmd",
args: args
cmd: "",
params: map[string]string,
,
want: "",
,
name: "no question mark",
args: args
cmd: "origin-cmd",
params: map[string]string
"name": "tiger",
,
,
want: "origin-cmd?name=tiger",
,
name: "has question mark 1",
args: args
cmd: "origin-cmd?",
params: map[string]string
"name": "tiger",
,
,
want: "origin-cmd?name=tiger",
,
name: "has question mark 2",
args: args
cmd: "origin-cmd?param1=value1",
params: map[string]string
"name": "tiger",
,
,
want: "origin-cmd?param1=value1&name=tiger",
,
for _, tt := range tests
t.Run(tt.name, func(t *testing.T)
if got := paddingExtraParams(tt.args.cmd, tt.args.params); got != tt.want
t.Errorf("paddingExtraParams() = %v, want %v", got, tt.want)
)
在我看来,适合 TDT 的大致有这么几种场景,感兴趣的可以试一试。
- 算法
- 无外部依赖
- 边界值较多
4.2 Mock
mock比较适合接口层的模拟,在测试包中创建一个结构体,去实现某个外部接口的依赖,以达到数据层模拟的效果。
先看下代码结构
在使用 mock 之前,需要先安装一下 gomock 库以及 mockgen 工具,可以通过如下命令进行安装。
go get github.com/golang/mock
比如,我们想对 car 这个 model 进行 mock,models/dao/car.go
package dao
type Car struct
Name string
Price int
type CarRepository interface
FindOne(id int)(Car, error)
然后使用 mockgen 工具生成 mock 代码
mockgen -source models/dao/car.go -destination test/models/dao/car_mock.go -package dao
# 如果上面的命令有问题的话,可以使用下面命令做下替换
mockgen -source car.go -destination car_mock.go -package dao
# 把 car_mock.go 放到 tests/models/dao/文件夹下即可,文件夹名称、路径均可自由发挥
就可以看到自动生成的代码car_mock.go
// Code generated by MockGen. DO NOT EDIT.
// Source: car.go
// Package mock_dao is a generated GoMock package.
package dao
import (
gomock "github.com/golang/mock/gomock"
dao "github.com/guoruibiao/unittest/models/dao"
reflect "reflect"
)
// MockCarRepository is a mock of CarRepository interface
type MockCarRepository struct
ctrl *gomock.Controller
recorder *MockCarRepositoryMockRecorder
// MockCarRepositoryMockRecorder is the mock recorder for MockCarRepository
type MockCarRepositoryMockRecorder struct
mock *MockCarRepository
// NewMockCarRepository creates a new mock instance
func NewMockCarRepository(ctrl *gomock.Controller) *MockCarRepository
mock := &MockCarRepositoryctrl: ctrl
mock.recorder = &MockCarRepositoryMockRecordermock
return mock
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockCarRepository) EXPECT() *MockCarRepositoryMockRecorder
return m.recorder
// FindOne mocks base method
func (m *MockCarRepository) FindOne(id int) (dao.Car, error)
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindOne", id)
ret0, _ := ret[0].(dao.Car)
ret1, _ := ret[1].(error)
return ret0, ret1
// FindOne indicates an expected call of FindOne
func (mr *MockCarRepositoryMockRecorder) FindOne(id interface) *gomock.Call
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockCarRepository)(nil).FindOne), id)
car_mock.go
本身即作为car.go
内部接口的替代,所以接下来就可以在实际的单测代码中看到如何使用。tests/models/dao/car_test.go
package dao
import (
"github.com/golang/mock/gomock"
"github.com/guoruibiao/unittest/models/dao"
"testing"
)
func TestOne(t *testing.T)
ctl := gomock.NewController(t)
defer ctl.Finish()
carRepo := NewMockCarRepository(ctl)
carRepo.EXPECT().FindOne(1).Return(dao.CarName: "保时捷", Price: 1000000, nil)
carRepo.EXPECT().FindOne(2).Return(dao.CarName: "法拉利", Price: 2500000, nil)
carRepo.EXPECT().FindOne(3).Return(dao.CarName: "宾利", Price: 9999999, nil)
// 结果校验
testcases := map[int]string
1: "保时捷",
2: "法拉利",
3: "宾利",
for id, want := range testcases
if car, err := carRepo.FindOne(id); car.Name != want
t.Error(err)
gomock 内容很多,感兴趣的去官网看看怎么用,我用的不是很多,没什么发言权,就不过多写了。
4.3 Stub
stub本意为桩,即在测试包中创建一个模拟方法,用于替换测试代码中调用层逻辑。
package dao
import (
"encoding/json"
"fmt"
"github.com/golang/mock/gomock"
"github.com/guoruibiao/unittest/models/dao"
"github.com/prashantv/gostub"
"testing"
"time"
)
func TestOne(t *testing.T)
ctl := gomock.NewController(t)
defer ctl.Finish()
carRepo := NewMockCarRepository(ctl)
carRepo.EXPECT().FindOne(1).Return(dao.CarName: "保时捷", Price: 1000000, nil)
carRepo.EXPECT().FindOne(2).Return(dao.CarName: "法拉利", Price: 2500000, nil)
carRepo.EXPECT().FindOne(3).Return(dao.CarName: "宾利", Price: 9999999, nil)
// 结果校验
testcases := map[int]string
1: "保时捷",
2: "法拉利",
3: "宾利",
for id, want := range testcases
if car, err := carRepo.FindOne(id); car.Name != want
t.Error(err)
func TestVariable(t *testing.T)
var number int
t.Log("before-stubed: ", number)
stubs := gostub.Stub(&number, 1008611)
defer stubs.Reset()
t.Log("after-stubed: ", number)
func Hello(name string) string
return fmt.Sprintf("Hello %s.", name)
func TestFunc(t *testing.T)
var HelloFunc = Hello
t.Log("Before stub:", HelloFunc("tiger"))
stubs := gostub.StubFunc(&HelloFunc, "tiger")
defer stubs.Reset()
t.Log("After stub:", HelloFunc("tiger"))
var MarshalFunc = json.Marshal
func TestOutterFunc(t *testing.T)
data := "A"
t.Log("Before stub:")
t.Log(MarshalFunc(data))
stubs := gostub.StubFunc(&MarshalFunc, func(v interface)([]byte, error)
return []byte("fake-news"), nil
)
defer stubs.Reset()
t.Log("After stub: ")
t.Log(MarshalFunc(data))
func TestOutterTime(t *testing.T)
var TimeFunc = time.Now
t.Log("Before stub: ", TimeFunc())
stubs := gostub.StubFunc(&TimeFunc, func()time.Time
d, _ := time.ParseDuration("+1h")
return time.Now().Add(d)
)
defer stubs.Reset()
t.Log("After stub: ", TimeFunc())
前几个方法没问题,后面对外部代码打桩时,发现会有错误。
=== RUN TestOutterFunc
TestOutterFunc: car_test.go:61: Before stub:
TestOutterFunc: car_test.go:62: [34 65 34] <nil>
--- FAIL: TestOutterFunc (0.00s)
panic: func type has 2 return values, but only 1 stub values provided [recovered]
panic: func type has 2 return values, but only 1 stub values provided
姑且当做一个待解决的问题吧。
4.4 Mock 与 Stub 的对比
从网上抄了一份别人整理的mock 与 stub 的对比,如下:
1)mock和stub都是采用替换的方式来实现,被测试的函数中的依赖关系,不过mock采用的是接口替换的方式,stub采用的是函数替代的方式。
2)mock的实现对功能代码没有侵入性,stub的侵入性比较强,在实现功能函数的时候,就需要为了测试设置一些回调函数,也就是这里所谓的桩。
3)对于控制被替代的方法来讲,mock如果想支持不同的输出,就需要提前实现不同的分支代码,甚至需要定义不同的mock结构体来实现,这样的mock代码会变成一个支持所有逻辑分支的一个最大集合,mock代码复杂性会变高;stub却能很好的控制桩函数的不同分支,因为stub替换的是函数,那么只要需要再用到这种输出的时候,定义一个函数即可,而这个函数甚至都可以是匿名函数。
4.5 monkey
monkey 是另一种打桩测试方法,个人感觉比 stub 好用,最起码看起来容易理解点。用之前需要进行安装。
go get bou.ke/monkey
package main
import (
"bou.ke/monkey"
"fmt"
)
var foo1 = func (s string) string
return fmt.Sprintf("Hello %s.", s)
var foo2 = func (s string) string
return fmt.Sprintf("HELLO %s", s)
func main()
monkey.Patch(foo1, foo2)
fmt.Println(foo1("Tiger"))
// 运行结果
HELLO Tiger
然后再来测试一个外部方法的桩测试。
package main
import (
"bou.ke/monkey"
"encoding/json"
"fmt"
)
var MarshalFunc = json.Marshal
func MarshalFuncOfMine(v interface)([]byte, error)
fmt.Println("---------------------")
fmt.Printf("%#v\\n", v)
fmt.Println("---------------------")
return json.Marshal(v)
func main()
monkey.Patch(MarshalFunc, MarshalFuncOfMine)
fmt.Println(MarshalFunc("Tiger"))
5 总结整理
最后来一起回顾一下,本文以痛点开始,介绍了开发同学在编写单测代码时可能的心路历程,然后通过分析单测内部的依赖类型,介绍了对应的较为适合的方案。
软件开发没有银弹,任何想要偷懒的行为本身就是错误的。软件也不是测试出来的,单纯依赖单测,而不主动设计代码结构,最后都会变得难以维护,成为后来者眼中的💩山。
建议阅读:go单元测试学习指南
以上是关于要想需求做的好,单测实践少不了。的主要内容,如果未能解决你的问题,请参考以下文章