什么是测试预言
测试预言是判断实际结果是否正确的依据。断言只是最后一步,真正决定对错的是其背后的规格说明、参考实现、不变量、黄金文件或人工评审。没有预言,测试只能执行代码,无法判断结果。所谓“预言问题”,指的是许多输出并不存在既便宜又完整的正确性来源。
常见的预言类型
规格预言从需求和示例推导期望值;派生预言借助模型、参考实现或蜕变关系,例如“对已排序列表再排序结果不变”;隐式预言依赖不变量:不崩溃、在预算内响应、无重复键、无未处理异常;人工预言仍用于可用性、文案和视觉布局。强测试套件会组合多种预言,避免单一错误期望掩盖缺陷。
如何选择
业务规则清晰时使用规格预言,输入空间很大时使用基于属性的预言,健壮性和性能检查使用隐式预言。与逐字符断言相比,不变量预言能容忍无害的格式变化;代价是精度下降:宽松的预言会放过更多错误输出,精确的预言则会因每次外观调整而失败。
常见陷阱
如果断言的值正是被测代码刚刚算出的值,测试就是同义反复;例如用序列化器自己的解析器验证其输出,只能证明自洽。从有缺陷的构建录制的黄金文件会把缺陷固化。只断言“没有抛异常”则会完全漏掉错误答案。应像审查生产代码一样审查期望值,有意识地更新黄金文件并说明原因,同时用已知故障验证预言确实会失败。
代码示例
package calc
import "testing"
func Divide(a, b float64) (float64, bool) {
if b == 0 {
return 0, false
}
return a / b, true
}
func TestDivide(t *testing.T) {
cases := []struct {
name string
a, b float64
want float64
wantOK bool
}{
{name: "even", a: 10, b: 2, want: 5, wantOK: true},
{name: "fraction", a: 1, b: 4, want: 0.25, wantOK: true},
{name: "divide by zero", a: 1, b: 0, want: 0, wantOK: false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, ok := Divide(tc.a, tc.b)
if ok != tc.wantOK || got != tc.want {
t.Fatalf("got (%v, %v), want (%v, %v)", got, ok, tc.want, tc.wantOK)
}
})
}
}
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)
}
})
}
}