Enumerating test cases
由于测试存储在 slice 中,我们可以在失败消息中打印出测试用例的索引:
func TestSplit(t *testing.T) { tests := []struct { input string sep . string want []string }{ {input: "a/b/c", sep: "/", want: []string{"a", "b", "c"}}, {input: "a/b/c", sep: ",", want: []string{"a/b/c"}}, {input: "abc", sep: "/", want: []string{"abc"}}, {input: "a/b/c/", sep: "/", want: []string{"a", "b", "c"}}, } for i, tc := range tests { got := Split(tc.input, tc.sep) if !reflect.DeepEqual(tc.want, got) { t.Fatalf("**test %d:** expected: %v, got: %v", **i+1**, tc.want, got) } } }
现在,当我们运行 go test
我们得到了这个:
% go test --- FAIL: TestSplit (0.00s) split_test.go:24: **test 4:** expected: [a b c], got: [a b c ]
这样好了一些。 现在我们知道第四个测试失败了,尽管我们不得不做了一点点捏造,因为 slice 索引和范围迭代是从 0 开始的。 这要求您的测试用例保持一致; 如果有些人从 0 开始报告而其他人使用 1 开始报告,那将会令人困惑。 并且,如果测试用例列表很长,则可能很难数大括号以确切地确定第4个测试用例由哪些结构构成。
Give your test cases names
另一种常见模式是在测试结构中包含名称字段。
func TestSplit(t *testing.T) { tests := []struct { **name string** input string sep string want []string }{ {name: "simple", input: "a/b/c", sep: "/", want: []string{"a", "b", "c"}}, {name: "wrong sep", input: "a/b/c", sep: ",", want: []string{"a/b/c"}}, {name: "no sep", input: "abc", sep: "/", want: []string{"abc"}}, {name: "trailing sep", input: "a/b/c/", sep: "/", want: []string{"a", "b", "c"}}, } for _, tc := range tests { got := Split(tc.input, tc.sep) if !reflect.DeepEqual(tc.want, got) { t.Fatalf("**%s:** expected: %v, got: %v", **tc.name**, tc.want, got) } } }
现在,当测试失败时,我们有一个描述性的名称,描述正在进行的测试。 我们不再需要尝试从输出中找出它 —— 现在还有一个字符串,我们可以搜索。
% go test --- FAIL: TestSplit (0.00s) split_test.go:25: **trailing sep**: expected: [a b c], got: [a b c ]
我们可以使用 map 字面值语法来更详细地说明这一点:
func TestSplit(t *testing.T) { tests := **map[string]struct { input string sep string want []string }**{ "simple": {input: "a/b/c", sep: "/", want: []string{"a", "b", "c"}}, "wrong sep": {input: "a/b/c", sep: ",", want: []string{"a/b/c"}}, "no sep": {input: "abc", sep: "/", want: []string{"abc"}}, "trailing sep": {input: "a/b/c/", sep: "/", want: []string{"a", "b", "c"}}, } for name, tc := range tests { got := Split(tc.input, tc.sep) if !reflect.DeepEqual(tc.want, got) { t.Fatalf("**%s:** expected: %v, got: %v", **name**, tc.want, got) } } }
使用 map 字面值语法,我们不再将测试用例定义为结构的 slice,而是作为测试名到测试结构的 map。 使用可能会提高测试效果的 map 还有一个好处。
map 迭代顺序是 undefined1 这意味着每次运行 go test
,我们的测试都可能以不同的顺序运行。
这对于发现在按语句顺序运行时测试通过的条件非常有用,但不适用于其他情况。如果您发现这种情况发生了,您可能是有一些全局状态,被一次测试改变,而后续测试取决于该修改。
Introducing sub tests
在我们修复失败的测试之前,还有一些其他问题需要在我们的 table driven test 工具中解决。
第一,我们在其中一个测试用例失败时调用t.Fatalf。 这意味着在第一次失败的测试用例之后我们停止测试其他情况。 因为测试用例是以未定义的顺序运行的,所以如果测试失败,那么知道它是唯一的失败还是只是第一次失败会更好。
如果我们努力将每个测试用例写出来作为测试包的函数,测试包将为我们做到这一点,但是这很冗长。 好消息是,自从Go 1.7添加了一项新功能,让我们可以轻松地进行 table driven test。 它们被称为 sub tests。
func TestSplit(t *testing.T) { tests := map[string]struct { input string sep string want []string }{ "simple": {input: "a/b/c", sep: "/", want: []string{"a", "b", "c"}}, "wrong sep": {input: "a/b/c", sep: ",", want: []string{"a/b/c"}}, "no sep": {input: "abc", sep: "/", want: []string{"abc"}}, "trailing sep": {input: "a/b/c/", sep: "/", want: []string{"a", "b", "c"}}, } for name, tc := range tests { **t.Run(name, func(t *testing.T) { got := Split(tc.input, tc.sep) if !reflect.DeepEqual(tc.want, got) { t.Fatalf("expected: %v, got: %v", tc.want, got) } })** } }
由于每个 sub test 现在都有一个名称,我们可以在任何测试运行中自动打印出该名称。
% go test --- FAIL: TestSplit (0.00s) --- FAIL: **TestSplit/trailing_sep** (0.00s) split_test.go:25: expected: [a b c], got: [a b c ]
每个 subtest 都是它自己的匿名函数,因此我们可以使用 t.Fatalf
,t.Skipf
和所有其他 testing.T
helper,同时保留table driven test 的紧凑性。