GoのTableDrivenTestsを読む

Introduction
テーブル駆動テストはあくまでテストを書くときの考え方
Table driven testing is not a tool, package or anything else, it’s just a way and perspective to write cleaner tests.
いいテストを書くのはそもそも容易ではないが「テーブル駆動テスト」を使うと、たくさんのケースを効率よく書くことができる。もし「コピペ」するだけでテストを書いているのであれば「テーブル駆動テスト」にリファクタリングするか、ヘルパー関数に切り出そう
Writing good tests is not trivial, but in many situations a lot of ground can be covered with table-driven tests: Each table entry is a complete test case with inputs and expected results, and sometimes with additional information such as a test name to make the test output easily readable. If you ever find yourself using copy and paste when writing a test, think about whether refactoring into a table-driven test or pulling the copied code out into a helper function might be a better option.
テストケースのテーブルを用意すれば、あとはテスト処理をテーブルでループするだけ。使いまわせるようにエラーメッセージを作り込んだテストにするのがベスト
Given a table of test cases, the actual test simply iterates through all table entries and for each entry performs the necessary tests. The test code is written once and amortized over all table entries, so it makes sense to write a careful test with good error messages.

Example of a table driven test
fmtパッケージのいい例(https://pkg.go.dev/fmt/)
var flagtests = []struct {
in string
out string
}{
{"%a", "[%a]"},
{"%-a", "[%-a]"},
{"%+a", "[%+a]"},
{"%#a", "[%#a]"},
{"% a", "[% a]"},
{"%0a", "[%0a]"},
{"%1.2a", "[%1.2a]"},
{"%-1.2a", "[%-1.2a]"},
{"%+1.2a", "[%+1.2a]"},
{"%-+1.2a", "[%+-1.2a]"},
{"%-+1.2abc", "[%+-1.2a]bc"},
{"%-1.2abc", "[%-1.2a]bc"},
}
func TestFlagParser(t *testing.T) {
var flagprinter flagPrinter
for _, tt := range flagtests {
t.Run(tt.in, func(t *testing.T) {
s := Sprintf(tt.in, &flagprinter)
if s != tt.out {
t.Errorf("got %q, want %q", s, tt.out)
}
})
}
}
t.Errorf
で何を入力して、何が期待されて、何を実際に得たのかをわかるようにして、テストコードを読まなくても問題が特定できるのがポイント。
Note the detailed error message provided with t.Errorf: its result and expected result are provided; the input is the subtest name. When the test fails it is immediately obvious which test failed and why, even without having to read the test code.
t.Errorf
はassertion(エラー時に即座にテストを中断する)ではないので、エラーが発生しても引き続きテストがされる。(他のケースでも落ちているものがあればそこから類推できるので幅広く情報を得るメリットがある)
A t.Errorf call is not an assertion. The test continues even after an error is logged. For example, when testing something with integer input, it is worth knowing that the function fails for all inputs, or only for odd inputs, or for powers of two.

Using a Map to Store Test Cases
struct[]
ではなくmap
を使うことも可能で、いくつかメリットもある
tests := map[string]struct {
input string
result string
} {
"empty string": {
input: "",
result: "",
},
"one character": {
input: "x",
result: "x",
},
"one multi byte glyph": {
input: "🎉",
result: "🎉",
},
"string with multiple multi-byte glyphs": {
input: "🥳🎉🐶",
result: "🐶🎉🥳",
},
}
for name, test := range tests {
// test := test // NOTE: uncomment for Go < 1.22, see /doc/faq#closures_and_goroutines
t.Run(name, func(t *testing.T) {
t.Parallel()
if got, expected := reverse(test.input), test.result; got != expected {
t.Fatalf("reverse(%q) returned %q; expected %q", test.input, got, expected)
}
})
}
maのindexをテスト名としてそのまま使うことができる
One advantage of using maps is that the “name” of each test can simply be the map index.
mapはループを回す時に順番がランダムになるので、テストの独立性が上がる(順番に依存しないので、テスト間の依存を見つけることができる)
More importantly, map iteration order isn’t specified nor is it even guaranteed to be the same from one iteration to the next. This ensures that each test is independent of the others and that testing order doesn’t impact results.

Parallel Testing
テーブルテストを並列かするのは簡単だが、慎重に書かないとバグになる。
特に以下の3つの変更点に注意
package main
import (
"testing"
)
func TestTLog(t *testing.T) {
t.Parallel() // このテスト関数自体を並列可能にする
tests := []struct {
name string
}{
{"test 1"},
{"test 2"},
{"test 3"},
{"test 4"},
}
for _, test := range tests {
// test := test // Go 1.22以前なら必要: /doc/faq#closures_and_goroutines
t.Run(test.name, func(t *testing.T) {
t.Parallel()// 各テストケースも並列実行可能にする
t.Log(test.name)
})
}
}

この記事も良かった