← 中文资源

简体中文版本

Go 表驱动单元测试

用表驱动测试覆盖函数全部分支:用例声明为数据、命名子测试并行执行、错误与结果双断言输出精确上下文。

阅读英文原文

示例说明

表驱动是 Go 测试的惯用形态:把用例收敛成结构体切片,断言逻辑只写一遍——新增用例只是加一行数据,不是复制一段测试函数。每个用例命名成子测试后可以单独 go test -run 精准重跑,失败信息自动带上用例名。t.Parallel 让子测试并发执行,大套件耗时显著缩短。

技术要点

  • 用例即数据,断言逻辑单份维护
  • 子测试命名支持精准选择执行
  • 错误分支与结果分支双断言

代码解读

测试表覆盖七条路径:轻重两档基础价、快递翻倍、超重加价、金牌折扣与两条错误边界。子测试先判 wantErr——期望出错的用例核对错误消息文本;正常用例核对金额。Fatalf 与 Errorf 的选择遵循失败即停与继续收集的差异。循环内捕获变量保证并行安全。

使用建议

错误消息比对用精确匹配时要同步维护文案,也可只断言错误出现。期望值手工计算易错,关键公式用属性测试交叉验证。

代码示例

package main

import (
	"errors"
	"testing"
)

var errNotFound = errors.New("order not found")

func ShippingCost(weightKg int, express bool, tier string) (int, error) {
	if weightKg <= 0 {
		return 0, errors.New("weight must be positive")
	}
	if weightKg > 30 {
		return 0, errors.New("weight exceeds courier limit")
	}

	base := 8
	if weightKg > 10 {
		base += (weightKg - 10) * 2
	}
	if express {
		base *= 2
	}
	if tier == "gold" {
		base -= 4
	}
	return base, nil
}

func TestShippingCost(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name    string
		weight  int
		express bool
		tier    string
		want    int
		wantErr error
	}{
		{name: "light standard", weight: 2, express: false, tier: "", want: 8},
		{name: "light express", weight: 2, express: true, tier: "", want: 16},
		{name: "heavy adds per kg", weight: 15, express: false, tier: "", want: 18},
		{name: "heavy express gold", weight: 15, express: true, tier: "gold", want: 32},
		{name: "gold floor at zero", weight: 1, express: false, tier: "gold", want: 4},
		{name: "zero weight rejected", weight: 0, express: false, tier: "", wantErr: errors.New("weight must be positive")},
		{name: "over courier limit", weight: 31, express: false, tier: "", wantErr: errors.New("weight exceeds courier limit")},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			got, err := ShippingCost(tt.weight, tt.express, tt.tier)
			if tt.wantErr != nil {
				if err == nil || err.Error() != tt.wantErr.Error() {
					t.Fatalf("ShippingCost(%d, %v, %q) error = %v, want %v",
						tt.weight, tt.express, tt.tier, err, tt.wantErr)
				}
				return
			}
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
			if got != tt.want {
				t.Errorf("ShippingCost(%d, %v, %q) = %d, want %d",
					tt.weight, tt.express, tt.tier, got, tt.want)
			}
		})
	}
}