是的,我又离职了。这一次离开的主要原因是与上家公司团队理念上的不合,可能还包括一些其他因素。真正让我决定离开的核心原因是这个团队乃至整个公司都不注重测试。这或许与公司产品定位有关,因为他们是一家游戏公司,需要快速抓住用户市场并推出爆品来盈利,导致产品周期迭代过快,根本没有多余时间进行测试。此外,缺乏一个好的发布流程和 Review 机制,从而导致生产事故频发。
测试的重要性
一般软件开发公司会专门设立 QA(Quality Assurance)工程师职位,专门负责把控产品质量。部分职责包括从业务角度(特别是边缘场景)进行测试,确保用户在使用产品过程中不会遇到问题及 bug,然后及时反馈并参与到迭代周期中,这是产品质量控制中不可或缺的一环。
单元测试的重要性
从开发工程师的角度把控产品质量的方式之一就是单元测试。开发者写的每个应用程序、业务流程、功能模块甚至每个独立的小单元,都必须通过单元测试确定是否达到预期。一些规范化的敏捷开发团队会将单元测试纳入 CI/CD 的持续集成中,实现了自动化构建部署一套体系。而在单元测试中,Mock 对象的使用非常普遍,尤其在需要隔离被测试代码与外部依赖(如数据库、网络服务、文件系统等)时。Mock 对象可以模拟这些外部依赖的行为,从而使测试更加独立和可控。今天我们就来了解一下 Go 语言单元测试中对于 Mock 所做的支持。
使用 Gomock 进行单元测试
Go 语言官方提供了一款 Mock 框架:Gomock 以及 Mock 文件生成工具:mockgen。它可以方便地生成模拟接口,支持与Go的内置 testing
包整合,也可以在其他上下文中使用。该项目源自 Google 的 golang/mock
仓库,但 Google 已不再维护。鉴于 Uber 内部对 Gomock 的大量使用,Uber 决定接手并继续维护该项目。我们将基于 uber-go/mock 做示例。
使用步骤
Gomock 的使用通常遵循以下四个基本步骤:
- 生成 Mock 文件:使用 mockgen 为你想要 mock 的接口生成一个 mock 文件。
- 创建 Mock 对象:在测试代码中,创建一个
gomock.Controller
实例,并将其作为参数传递给 mock 对象的构造函数来创建 mock 对象。 - 设置期望:调用
EXPECT()
为 mock 对象设置期望和返回值。 - 验证期望:调用 mock 控制器的
Finish()
方法验证 mock 的期望行为(通常在defer
中完成)。
示例
通过一个简单的例子演示 Gomock 的使用流程。假设你有一个简单的服务接口 UserService
,它有一个方法 GetUser
,通过用户ID获取用户信息。我们将为这个接口创建一个 mock 对象并编写测试代码。
首先,在项目中安装 mockgen,并加入到环境变量中:
go install go.uber.org/mock/mockgen@latest
# 配置环境变量
...
go get -u go.uber.org/mock/gomock
go get -u github.com/stretchr/testify/assert
接着,在 service/user.go
中定义一个接口,里面有一个 GetUser() string
方法,模拟获取信息。
package service
type User struct {
ID int
Name string
}
type UserService interface {
GetUser(id int) (*User, error)
}
使用 Mockgen 生成 UserService
的 mock 对象:
mockgen -source=service/user.go -destination=service/mock_user.go -package=service
这里的 destination
参数通常设置成与 source
参数文件同级目录,否则运行 go test
会出现 import cycle not allowed in test
,也就是循环导入错误;package
包名一般跟目录名相同即可。假设我们有一个函数 GetUserName
,它使用 UserService
接口实例中的方法来获取用户并返回用户的名字:
package service
func GetUserName(us UserService, id int) (string, error) {
// 使用依赖注入进来的具体结构体方法,通常为需要 mock 的方法
user, err := us.GetUser(id)
if err != nil {
return "", err
}
return user.Name, nil
}
在测试文件中编写测试代码:
package service
import (
"errors"
"github.com/stretchr/testify/assert"
"testing"
"go.uber.org/mock/gomock"
)
func TestGetUserName(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockUserService := NewMockUserService(ctrl)
// 定义测试用例
testCases := []struct {
name string
userID int
mockSetup func()
expected string
err error
}{
{
name: "valid user",
userID: 1,
mockSetup: func() {
mockUserService.EXPECT().
GetUser(1).
Return(&User{ID: 1, Name: "Alice"}, nil)
},
expected: "Alice",
err: nil,
},
{
name: "user not found",
userID: 2,
mockSetup: func() {
mockUserService.EXPECT().
GetUser(2).
Return(nil, errors.New("user not found"))
},
expected: "",
err: errors.New("user not found"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tc.mockSetup()
result, err := GetUserName(mockUserService, tc.userID)
assert.Equal(t, tc.expected, result)
assert.Equal(t, tc.err, err)
})
}
}
在根目录下运行 go test -v ./...
可以看到测试结果:
fantasticbin@Fantasticbins-MacBook-Pro gomock-demo % go test -v ./...
=== RUN TestGetUserName
=== RUN TestGetUserName/valid_user
=== RUN TestGetUserName/user_not_found
--- PASS: TestGetUserName (0.00s)
--- PASS: TestGetUserName/valid_user (0.00s)
--- PASS: TestGetUserName/user_not_found (0.00s)
PASS
ok gomock-demo/service 0.635s
每个测试场景全部通过,至此 Gomock 的使用示例介绍完毕。
其他 Mock 解决方案
目前在国内的 Go 语言 Mock 解决方案中,有一种较为新颖的玩法,即 Monkey Patching,翻译过来就是叫“猴子补丁”。简单来说,就是在不手动修改程序代码的情况下,对某个对象进行原地替换。这种方式有别于 Gomock 的面向接口编程思想,虽然不符合 SOLID 中的依赖倒置原则,但也有其使用场景,有兴趣的童鞋可以参考这篇文章 —— 《测试代码终极解决方案 Monkey Patching》
这篇文章的作者在近期提到了一款可以实现 Monkey Patching 的新工具:xgo。它是一款强大的Go测试工具集,功能包括Trap、Mock、Trace、增量覆盖率,支持所有 Go 语言支持的操作系统和架构。最重要的是,xgo 是一种并发安全的 Monkey Patching 方案,这也是其相较于其他方案的一大亮点。具体使用方式可以参考它的文档以及这篇文章 ——《xgo: 一款新鲜出炉的Go代码测试利器》。
Ending
可以看到国内外开发者在 Go 语言单元测试领域所作出的努力,对改善项目质量提供的帮助很大,尤其在国内某些企业不重视测试的情况下,能够提前发现和解决开发流程中的隐藏问题。希望这篇文章能帮助你理解和应用 Go 语言单元测试中的 Mock 技巧。
本文中所有代码示例可通过 点击这里 具体查看