👻

Go で標準出力に書き込まれた文字列のテストをする方法

2021/01/06に公開

はじめに

この記事では標準出力に書き出した文字列をユニットテストでテストする方法を紹介します。この方法を使うと fmt.Print や fmt.Printf 関数で出力した文字列のテストできるようになります。

文字列の出力先を標準出力からBufferに変えてテストを実施する

Go では os.Pipe 関数を使って標準出力や標準エラー出力の出力先を変更することができます。
この関数を使って、fmt.Print など標準出力(os.Stdout)に出力された文字列が想定通りかどうか確認するテストを書いてみます。

テスト対象の関数

テスト対象のコードです。fmt.Print 関数を使って文字列を出力するだけのシンプルな関数です。

// テスト対象の関数
func Print() {
	fmt.Print("test1")
	fmt.Print("test2")
}

この関数を実行すると標準出力に test1test2 という文字が出力されます。

テスト用のヘルパー関数を用意します

テストを実施する前に以下のようなテスト用のヘルパー関数を用意してあげます。ヘルパー関数の中で os.Pipe 関数を使います。

// Stdoutに書き込まれた文字列を抽出する関数
// (Stderrも同じ要領で出力先を変更できます)
func extractStdout(t *testing.T, fnc func()) string {
	t.Helper()

	// 既存のStdoutを退避する
	orgStdout := os.Stdout
	defer func() {
		// 出力先を元に戻す
		os.Stdout = orgStdout
	}()
	// パイプの作成(r: Reader, w: Writer)
	r, w, _ := os.Pipe()
	// Stdoutの出力先をパイプのwriterに変更する
	os.Stdout = w
	// テスト対象の関数を実行する
	fnc()
	// Writerをクローズする
	// Writerオブジェクトはクローズするまで処理をブロックするので注意
	w.Close()
	// Bufferに書き込こまれた内容を読み出す
	var buf bytes.Buffer
	if _, err := buf.ReadFrom(r); err != nil {
		t.Fatalf("failed to read buf: %v", err)
	}
	// 文字列を取得する
	return strings.TrimRight(buf.String(), "\n")
}

ヘルパー関数として独立させたのはテスト実施後に確実に Stdout の出力先を戻すためです。出力先を戻さないと他のテストを実施したときにログの確認ができなくなるので注意が必要です。あとヘルパー関数なんで t.Helper() を忘れないようにしましょう。

テストの実装

上記ヘルパー関数を使ったユニットテストは以下のようになります。

// テスト関数
func TestFoo(t *testing.T) {
	// テスト対象の関数が出力した文字列を取得する
	got := extractStdout(t, Print)
	// 想定される文字列
	want := "test1test2"
	if got != want {
		t.Errorf("failed to test. got: %s, want: %s", got, want)
	}
}

まとめ

  • os.Pipe を使うと os.Stdout から別の出力先に向きを変えることができる
  • 出力先を変更したら必ず戻さないと他のテストに影響するので気を付けること

Unixのシステムコールに詳しければ簡単に思いつく方法かもしれませんが、初めてこの方法を知ったときは結構驚きました。今回紹介したテスト方法はログの確認なんかで使える技なので意外と使いところがあると思います。

補足:ヘルパー関数はos.Pipeを使って何をしているか

Go の os.Pipe 関数を使うと OS のファイルデスクリプタにパイプを作成することができます(Unix の pipe システムコール呼び出し)。Go の os.Stdout の実態は標準出力に接続されたファイルオブジェクト(os.File)です。このオブジェクトをパイプの Writer オブジェクトに置き換えて出力先を変更しています。以下はプログラム実行時とテスト実行時の違いを図にしたものです。

pipeシステムコール

プログラム実行時

  • fmt.Print 関数は標準出力(ファイルデスクリプタのインデックス1)に文字列を出力する

テスト実行時

  • os.Pipe 関数を使ってパイプを作成する
    • ファイルデスクリプタのインデックス3にReader、インデックス4にWriterが作成される
  • os.Stdout オブジェクトをパイプの Writer オブジェクトに置き換える
  • テスト対象の関数を実行する
    • fmt.Print 関数が文字列を出力する
  • パイプの Writer オブジェクトをクローズする
  • パイプの Reader オブジェクトの内容を bytes.Buffer を使って読み出す

参考

Discussion