[Go] os.Stdin・os.Stdoutを用いた関数・メソッドのテスト

# テストケースの失敗例と解決策
Go言語でサポートされているテスト自動化ツール`go test`において，標準入出力を用いた関数・メソッドのテストを行う際にはひと工夫必要になる．

例として以下の関数をテストする場合を考える．
```golang:sample.go
/* パッケージ，インポート文は省略 */

// 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`を返す．

このように標準入出力を利用する関数のテストは，単純に書いても意図した動作にならないことがある．以下がそのようなテストのコードである．
```golang:sample_test.go
/* パッケージ，インポート文は省略 */

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`を返している．

今度は上記のテストケースに変更を加え，テスト内から直接標準入力に入力をさせてみる．
```golang:sample_test.go
/* パッケージ，インポート文は省略 */

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`に差し替えることで，テスト対象の関数を書き換えることなくテストが行える．
具体的には以下の通り．
```golang:sample_test.go
/* パッケージ，インポート文は省略 */

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()`側からも正しくスキャンできていることがわかる．

(終わり)