テストケースの失敗例と解決策
Go言語でサポートされているテスト自動化ツールgo test
において,標準入出力を用いた関数・メソッドのテストを行う際にはひと工夫必要になる.
例として以下の関数をテストする場合を考える.
/* パッケージ,インポート文は省略 */
// isFooは標準入力から文字列を受け取り,
// それが"Foo"と等しければtrueを,異なればfalseを返す.
// 比較の結果に加え,エラーが発生した場合はそれを,そうでなければnilを返す.
func isFoo() (bool, error) {
// 標準入力を読み取るScannerを取得.
scanner := bufio.NewScanner(os.Stdin)
// スキャンを行い,エラーが有ればそれを返す.
scanner.Scan()
if err := scanner.Err(); err != nil {
return false, err
}
// スキャンした文字列と"Foo"を比較した結果を返す.
return scanner.Text() == "Foo", nil
isFoo()
は呼び出されると標準入力を読み取り,入力文字列が文字列"Foo"
と等しい場合にはtrue
を返す.
このように標準入出力を利用する関数のテストは,単純に書いても意図した動作にならないことがある.以下がそのようなテストのコードである.
/* パッケージ,インポート文は省略 */
func Test_isFoo_Bad1(t *testing.T) {
// テストケースを用意する.
tests := []struct {
name string
want bool
wantErr bool
}{
{
name: "FooInput_returnTrueNil",
want: true,
wantErr: false,
},
{
name: "nonFooInput_returnFalseNil",
want: false,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// isFoo()を呼び出すことで標準入力を読み取りたいが...
got, err := isFoo()
if (err != nil) != tt.wantErr {
t.Errorf("isFoo() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("isFoo() = %v, want %v", got, tt.want)
}
})
}
}
上記のテストを実行すると,
$ go test -v -run isFoo_Bad1
=== RUN Test_isFoo_Bad1
=== RUN Test_isFoo_Bad1/FooInput_returnTrueNil
sample_test.go:260: isFoo() = false, want true
=== RUN Test_isFoo_Bad1/nonFooInput_returnFalseNil
--- FAIL: Test_isFoo_Bad1 (0.00s)
--- FAIL: Test_isFoo_Bad1/FooInput_returnTrueNil (0.00s)
--- PASS: Test_isFoo_Bad1/nonFooInput_returnFalseNil (0.00s)
FAIL
exit status 1
FAIL example.com/sample 0.002s
上の実行結果のように,入力を受け付ける間もなくテストが終了する.
それにもかかわらず,Scanner.Err()
は何事も無かったかのようにnil
を返している.
今度は上記のテストケースに変更を加え,テスト内から直接標準入力に入力をさせてみる.
/* パッケージ,インポート文は省略 */
func Test_isFoo_Bad2(t *testing.T) {
// テストケースを用意する.
tests := []struct {
name string
// 標準入力へ入力する文字列
input string
want bool
wantErr bool
}{
{
name: "FooInput_returnTrueNil",
input: "Foo",
want: true,
wantErr: false,
},
{
name: "nonFooInput_returnFalseNil",
input: "foo",
want: false,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// テスト内から直接標準入力へ入力してみる.
fmt.Fprintln(os.Stdout, tt.input)
got, err := isFoo()
if (err != nil) != tt.wantErr {
t.Errorf("isFoo() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("isFoo() = %v, want %v", got, tt.want)
}
})
}
}
先程と同様にテストを実行すると.
$ go test -v -run isFoo_Bad2
=== RUN Test_isFoo_Bad2
=== RUN Test_isFoo_Bad2/FooInput_returnTrueNil
Foo
sample_test.go:300: isFoo() = false, want true
=== RUN Test_isFoo_Bad2/nonFooInput_returnFalseNil
foo
--- FAIL: Test_isFoo_Bad2 (0.00s)
--- FAIL: Test_isFoo_Bad2/FooInput_returnTrueNil (0.00s)
--- PASS: Test_isFoo_Bad2/nonFooInput_returnFalseNil (0.00s)
FAIL
exit status 1
FAIL example.com/sample 0.002s
入力されている形跡はあるが,こちらも失敗する.
isFoo()
の最後のreturn
の前に,fmt.Fprintf(os.Stderr, "string scanned: \"%v\"\n", scanner.Text())
として,標準エラーにスキャンの結果を出力して確認すると,
$ go test -run isFoo_Bad2 -v
=== RUN Test_isFoo_Bad2
=== RUN Test_isFoo_Bad2/FooInput_returnTrueNil
Foo
string scanned: ""
sample_test.go:302: isFoo() = false, want true
=== RUN Test_isFoo_Bad2/nonFooInput_returnFalseNil
foo
string scanned: ""
--- FAIL: Test_isFoo_Bad2 (0.00s)
--- FAIL: Test_isFoo_Bad2/FooInput_returnTrueNil (0.00s)
--- PASS: Test_isFoo_Bad2/nonFooInput_returnFalseNil (0.00s)
FAIL
exit status 1
FAIL example.com/sample 0.002s
上の実行結果のように,isFoo()
側では空文字列""
しか読み取れていない.
どうやらテストの実行中は,標準入出力が通常とは異なる挙動を取ると推測される.
この問題への対処法として,テストの実行中だけos.Stdin
,os.Stdout
を別のReader
,Writer
に差し替えることで,テスト対象の関数を書き換えることなくテストが行える.
具体的には以下の通り.
/* パッケージ,インポート文は省略 */
func Test_isFoo_Good(t *testing.T) {
// テスト用に一時的なReader,Writerを取得する.
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("os.Pipe(): %v", err)
}
// os.Stdin,os.Stdoutを取得したReader,Writerと差し替え,テスト終了時に復元する.
osStdin, osStdout := os.Stdin, os.Stdout
os.Stdin, os.Stdout = r, w
defer func() { os.Stdin, os.Stdout = osStdin, osStdout }()
// テストケースを用意する.入力は末尾の改行'\n'を忘れずに!
tests := []struct {
name string
input string
want bool
wantErr bool
}{
{
name: "FooInput_returnTrueNil",
input: "Foo\n",
want: true,
wantErr: false,
},
{
name: "nonFooInput_returnFalseNil",
input: "foo\n",
want: false,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Writer.Write()に渡せるよう,入力を[]byteに変換する.
input := []byte(tt.input)
// 標準出力へ書き込む.
if n, err := os.Stdout.Write(input); err != nil {
t.Errorf("input is %v bytes, but only %v byte written", len(input), n)
return
}
got, err := isFoo()
if (err != nil) != tt.wantErr {
t.Errorf("isFoo() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("isFoo() = %v, want %v", got, tt.want)
}
})
}
}
テストを実行すると,
$ go test -run isFoo_Good -v
=== RUN Test_isFoo_Good
=== RUN Test_isFoo_Good/FooInput_returnTrueNil
string scanned: "Foo"
=== RUN Test_isFoo_Good/nonFooInput_returnFalseNil
string scanned: "foo"
--- PASS: Test_isFoo_Good (0.00s)
--- PASS: Test_isFoo_Good/FooInput_returnTrueNil (0.00s)
--- PASS: Test_isFoo_Good/nonFooInput_returnFalseNil (0.00s)
PASS
ok example.com/sample 0.002s
テストは全てパスし,isFoo()
側からも正しくスキャンできていることがわかる.
(終わり)