golang推荐使用表格驱动测试的原因有三点:首先,它提高了代码的可读性和维护性,所有测试用例集中在一个数据结构中,添加新用例只需在表格加一行。其次,错误报告更清晰,通过t.run为每个用例创建子测试,失败时能明确指出具体哪个用例出错。最后,它支持并行测试,调用t.parallel()可提升效率,但需确保用例间无共享状态。

Golang的
testing

说起来,其实很简单,我们先定义一个要测试的函数。就拿一个最简单的加法函数来说吧:
// main.go
package main
func Add(a, b int) int {
return a + b
}
func Subtract(a, b int) int {
return a - b
}接着,我们就可以为它编写表格驱动的单元测试了。通常,测试文件会以
_test.go
main_test.go
立即学习“go语言免费学习笔记(深入)”;

// main_test.go
package main
import (
"fmt"
"testing"
)
func TestAdd(t *testing.T) {
// 定义测试用例结构体
type args struct {
a int
b int
}
type testCase struct {
name string // 测试用例的名称,方便识别
args args // 输入参数
want int // 期望的输出结果
}
// 编写测试用例表
tests := []testCase{
{
name: "基本加法",
args: args{a: 1, b: 2},
want: 3,
},
{
name: "负数加法",
args: args{a: -1, b: -2},
want: -3,
},
{
name: "零值加法",
args: args{a: 0, b: 5},
want: 5,
},
{
name: "大数加法",
args: args{a: 1000000, b: 2000000},
want: 3000000,
},
}
// 遍历测试用例并执行
for _, tt := range tests {
// 使用 t.Run 为每个测试用例创建一个子测试
// 这样即使某个子测试失败,其他子测试也能继续运行,报告也更清晰
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.args.a, tt.args.b)
if got != tt.want {
// 如果结果不符合预期,报告错误
t.Errorf("Add() for test case %q = %v, want %v", tt.name, got, tt.want)
}
})
}
}
func TestSubtract(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"positive numbers", 5, 3, 2},
{"negative numbers", -5, -3, -2},
{"zero result", 10, 10, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Subtract(tt.a, tt.b)
if got != tt.want {
t.Errorf("Subtract(%d, %d) got %d, want %d", tt.a, tt.b, got, tt.want)
}
})
}
}
// 示例:如何使用 t.Fatalf 立即停止测试
func TestFatalError(t *testing.T) {
// 假设这里有一个前置条件检查
if false { // 模拟一个致命错误条件
t.Fatalf("致命错误:无法初始化测试环境") // t.Fatalf 会打印错误并立即停止当前测试函数
}
t.Log("致命错误后的代码不会执行")
}
// 示例:如何使用 t.Log 打印调试信息
func TestLogInfo(t *testing.T) {
result := 42
t.Logf("计算结果是: %d", result) // t.Logf 会在测试通过时也打印信息,方便调试
if result != 42 {
t.Errorf("结果不正确")
}
}运行测试很简单,在项目根目录执行
go test -v
-v
这就像是,你有一堆形状各异的积木,每个积木都代表一个测试场景。如果为每个积木都建一个独立的盒子,那盒子会堆满屋子,找起来也麻烦。但如果把所有积木的信息都写在一张清单上,然后用一个统一的流程去检查它们,是不是就清晰多了?在我看来,表格驱动测试就是那张高效的清单。

首先,它极大地提高了代码的可读性和维护性。所有的测试用例都集中在一个数据结构里,一目了然。当你想添加一个新的测试场景时,只需要在表格里加一行,而不用复制粘贴一大段代码,这大大减少了冗余。我个人觉得这非常有用,特别是当函数有很多不同的输入组合时。
其次,错误报告会更清晰。通过
t.Run()
TestAdd
还有,它方便并行测试。在
t.Run
t.Parallel()
测试这事儿,总有些坑要避开,也有一些好习惯值得培养。
一个常见的陷阱就是不使用 t.Run
for
if got != want
TestXxx
t.Errorf
t.Run
另一个坑是在并行测试中修改共享状态。如果你在
t.Run
t.Parallel()
测试覆盖率不是唯一标准。有些人只看覆盖率数字,但覆盖率高不代表测试质量就高。一个好的测试应该覆盖到各种边界条件、错误路径、以及那些“不可能发生”的异常情况。比如,测试一个除法函数,你得考虑除数为零的情况;测试一个字符串解析函数,你得考虑空字符串、非法格式的字符串。
关于最佳实践,我个人有几点体会:
TestFunctionName
TestFunctionName_Scenario
TestAdd_NegativeNumbers
t.Run(tt.name, ...)
tt.name
testdata
testdata
os.ReadFile
testdata
在实际项目中,函数往往不是孤立的,它们可能依赖数据库、外部API、文件系统,甚至当前时间。这些都是副作用,会使测试变得复杂且不稳定。处理这些依赖是单元测试的另一个核心挑战。
一个非常核心的思路是依赖注入 (Dependency Injection, DI)。与其让函数直接创建或访问这些外部资源,不如通过函数参数或结构体字段把它们“注入”进来。这样,在测试时,你就可以注入一个“假”的(mock 或 stub)依赖,而不是真实的。
例如,如果你的函数需要访问数据库:
// service.go
package main
type User struct {
ID int
Name string
}
// 定义一个接口,代表数据库操作
type UserStore interface {
GetUserByID(id int) (*User, error)
SaveUser(user *User) error
}
type UserService struct {
store UserStore // 注入 UserStore 接口
}
func (s *UserService) GetUserName(id int) (string, error) {
user, err := s.store.GetUserByID(id)
if err != nil {
return "", err
}
return user.Name, nil
}在测试中,我们可以创建一个假的
UserStore
// service_test.go
package main
import (
"errors"
"testing"
)
// MockUserStore 是 UserStore 接口的模拟实现
type MockUserStore struct {
getUserByIDFunc func(id int) (*User, error)
saveUserFunc func(user *User) error
}
func (m *MockUserStore) GetUserByID(id int) (*User, error) {
if m.getUserByIDFunc != nil {
return m.getUserByIDFunc(id)
}
return nil, errors.New("not implemented")
}
func (m *MockUserStore) SaveUser(user *User) error {
if m.saveUserFunc != nil {
return m.saveUserFunc(user)
}
return errors.New("not implemented")
}
func TestGetUserName(t *testing.T) {
tests := []struct {
name string
userID int
mockGetUser func(id int) (*User, error) // 注入模拟函数
wantName string
wantErr bool
}{
{
name: "用户存在",
userID: 1,
mockGetUser: func(id int) (*User, error) {
if id == 1 {
return &User{ID: 1, Name: "Alice"}, nil
}
return nil, errors.New("user not found")
},
wantName: "Alice",
wantErr: false,
},
{
name: "用户不存在",
userID: 2,
mockGetUser: func(id int) (*User, error) {
return nil, errors.New("user not found")
},
wantName: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockStore := &MockUserStore{
getUserByIDFunc: tt.mockGetUser,
}
service := &UserService{store: mockStore}
gotName, err := service.GetUserName(tt.userID)
if (err != nil) != tt.wantErr {
t.Errorf("GetUserName() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotName != tt.wantName {
t.Errorf("GetUserName() gotName = %v, want %v", gotName, tt.wantName)
}
})
}
}对于文件系统操作,可以利用
io/ioutil
os
TempDir
RemoveAll
至于时间依赖,比如
time.Now()
总的来说,处理依赖和副作用的核心思想是:隔离。通过接口、依赖注入、临时资源等手段,让你的被测单元与外部世界解耦,从而保证测试的纯粹性、可重复性和稳定性。这有点意思,因为你不是在测试真实世界,而是在一个受控的微观世界里验证你的代码行为。
以上就是Golang的testing库如何编写单元测试 讲解表格驱动测试的写法的详细内容,更多请关注php中文网其它相关文章!
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号