一、背景和意义
过于依赖集成测试可能存在测试流程较长以及某些场景/条件覆盖不到的问题,相比之下单元测试则是提升测试速度与程序质量的重要工具。go test是go语言官方的单元测试工具,可实现简单的单元测试功能。而对于一些大量使用依赖注入的复杂系统,做单元测试是总少不了mock框架,而go语言也提供了gomock这一官方的mock框架。
本文将提供一个go test与gomock结合做单元测试的示例。
二、go test的使用
创建一个空目录,执行如下命令创建go模块:
bash
go mod init go-test-demo
然后创建文件 math/math.go:
go
package math
func Add(a, b int) int {
return a + b
}
再创建对应的测试文件 match/math_test.go
go
package math
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) return %d, expected: %d", result, expected)
}
}
创建完这两个文件之后执行命令:
bash
go test ./math
命令将会输出:
log
ok go-test-demo/math 0.097s
三、不同包下的go test使用
刚才创建了一个math包,现在再创建另外一个包。先创建文件 list/sum_list.go:
go
package list
import "go-test-demo/math"
func SumList(list []int) int {
sum := list[0]
for i := 1; i < len(list); i++ {
sum = math.Add(sum, list[i])
}
return sum
}
再创建对应的测试代码文件 list/sum_list_test.go:
go
package list
import "testing"
func TestSumList(t *testing.T) {
result := SumList([]int{2, 3, 4, 5})
expected := 14
if result != expected {
t.Errorf("SumList(2, 3, 4, 5) return %d, expected: %d", result, expected)
}
}
如下命令将执行 list下的单元测试:
bash
go test ./list
执行完命令将会输出
log
ok go-test-demo/list 0.070s
如果想执行两个包下的单元测试,则使用命令:
bash
go test ./list ./math
将会输出:
log
ok go-test-demo/list 0.094s
ok go-test-demo/math 0.104s
如果想要执行所有包下的单元测试,则是使用命令:
bash
go test ./...
四、需要使用mock的场景
在复杂系统中,经常涉及到依赖注入,这种情况下需要用mock。举个例子,创建票务服务相关的代码文件 biz/ticket_service.go:
go
package biz
type TicketService interface {
CanBuy(userId int, amount int) bool
}
type TickerServiceImpl struct {
financeService FinanceService
priceService PriceService
}
func NewTicketService(financeService FinanceService, priceService PriceService) TicketService {
return &TickerServiceImpl{
financeService: financeService,
priceService: priceService,
}
}
func (t *TickerServiceImpl) CanBuy(userId int, amount int) bool {
price := t.priceService.GetPrice("ticket")
balance := t.financeService.GetAccountBalance(userId)
return price*amount <= balance
}
票务服务需要依赖财务服务 FinanceService 和价格服务 PriceService 。财务服务的代码文件是 biz/finance_service.go:
go
package biz
type FinanceService interface {
GetAccountBalance(userId int) int
}
价格服务的代码文件是 biz/price_service.go:
go
package biz
type PriceService interface {
GetPrice(productName string) int
}
五、gomock的使用
这里将以前面提到的票务服务的例子介绍gomock的使用,先安装gomock:
bash
go install github.com/golang/mock/mockgen@latest
使用 mockgen 工具生成mock代码:
bash
mockgen -destination="mocks/mock_finance_service.go" -source ./biz/finance_service.go
mockgen -destination="mocks/mock_price_service.go" -source ./biz/price_service.go
执行结束之后,可以看到代码目录下生成了mocks/mock_finance_service.go和mocks/mock_price_service.go两个文件,接下来再执行添加依赖的命令:
bash
go mod tidy
创建测试用例文件 test/biz/ticket_service_test.go:
go
package biz
import (
"github.com/golang/mock/gomock"
"go-test-demo/biz"
mock_biz "go-test-demo/mocks"
"testing"
)
func TestCanBuy(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
// 先设置依赖服务期望的返回结果
userId := 111
financeService := mock_biz.NewMockFinanceService(mockCtrl)
financeService.EXPECT().GetAccountBalance(userId).Return(100).AnyTimes()
priceService := mock_biz.NewMockPriceService(mockCtrl)
priceService.EXPECT().GetPrice("ticket").Return(10).AnyTimes()
ticketService := biz.NewTicketService(financeService, priceService)
amount := 10
result := ticketService.CanBuy(userId, amount)
if result != true { // 一张票价格10,用户余额100,预期能买10张,即返回true
t.Errorf("Expect user %d can by %d tickets, buy actually is not", userId, amount)
}
ticketService = biz.NewTicketService(financeService, priceService)
amount = 11
result = ticketService.CanBuy(userId, amount)
if result != false { // 预期不能买11张,即返回false
t.Errorf("Expect user %d can not by %d tickets, buy actually is not", userId, amount)
}
}
执行测试用例:
bash
go test ./test/...
运行结果:
log
ok go-test-demo/test/biz 0.085s