この記事はGo4 Advent Calendar 2017の2日目の記事です。
現在業務で携わっているLinuxプログラミングについて書く予定でしたが、あまり関係のないテストについて書いてみます。
なお11月からGoを触りはじめたばかりなので、ツッコミなどあればどんどんコメントいただければ幸いです。
追記
この記事の内容をパッケージ化しました。
よかったらあわせて見ていただければと思います。
はじめに
fmt.Print()
などの出力をテストしたい場合、次のように fmt.Fprint()
を用いて io.Writer
を指定する形で文字列を出力し、通常実行時はこれに os.Stdout
をセットしつつテストでは *bytes.Buffer
に差し替えることで、その内容を検証することができます。
main.go
:
var writer io.Writer
func init() {
writer = os.Stdout
}
func fprint(a ...interface{}) {
fmt.Fprint(writer, a...)
}
次のように io.Writer
として *bytes.Buffer
をセットすると、文字列が出力された際に buffer.String()
をとおして参照することができます。
main_test.go
:
var buffer *bytes.Buffer
func init() {
buffer = &bytes.Buffer{}
writer = buffer
}
こういった実装ができればよいのですが、 fmt.Print()
を使用した(かつ自分の裁量による変更が難しい)既存のコードに対するテストを書く場合や、テスト実行時に外部パッケージが os.Stdout
あるいは os.Stderr
に出力してしまう場合など、出力をキャプチャまたは抑制したいケースがあるかもしれません。
これは os.Stdout
や os.Stderr
に別途 *os.File
型の値をセットすることで実現できるのですが、ここでは例をまじえてまとめたいと思います。
例: 加算を行うプログラム
例として、まず次のような足し算を行う関数と、それを標準出力に表示する関数をもつ main.go
があるとします。
package main
import (
"flag"
"fmt"
"os"
)
const (
ExitCodeOK = iota
ExitCodeError
)
func main() {
os.Exit(run(os.Args[1:]))
}
func sum(x, y int) int {
return x + y
}
func run(args []string) int {
var x, y int
flags := flag.NewFlagSet("sum", flag.ContinueOnError)
flags.IntVar(&x, "x", 0, "Value for x")
flags.IntVar(&y, "y", 0, "Value for y")
flags.Parse(args)
fmt.Printf("%d + %d = %d\n", x, y, sum(x, y))
return ExitCodeOK
}
コマンドライン引数のパースにflagパッケージを用いていますが、ここではさらに flag.NewFlagSet()
により新しいパーサを作成しています。
これにより flags.Parse()
にコマンドライン引数を渡せるようになり、テストが書きやすくなります。
このコードを実行すると、次のような結果が得られます。
$ go run main.go -x 2 -y 3
2 + 3 = 5
テストコード
この main.go
に対するテストコード main_test.go
の実装方針として、次の2つについて書くとよさそうです。
- ユニットテスト:
sum()
の返り値をテストする - インテグレーションテスト:
run()
の出力をテストする
それぞれについて書きます。
ユニットテスト
ユニットテストは、標準のtestingパッケージを使って適当に書くと次のようになると思います。
package main
import "testing"
func TestSum(t *testing.T) {
if sum(2, 3) != 5 {
t.Errorf("sum(2, 3) = %d, want 5", sum(2, 3))
}
}
インテグレーションテスト
この小さなコードでインテグレーションテストといっていいか分かりませんが、「 sum()
した結果を fmt.Printf()
する」という統合的な処理をテストする場合を考えます。
コマンドライン引数として -x 2 -y 3
を渡した際に 2 + 3 = 5
という結果が表示されることをテストしたいのですが、このままだとコンソールに結果が表示されるだけで、コード上の値としてこれを得ることはできません。
この標準出力自体をテストしたい場合、どうすればよいか……ということですが、私は次のようなヘルパを書いて対応しています。
package main
import (
"bytes"
"io"
"os"
)
func captureStdout(f func()) string {
r, w, err := os.Pipe()
if err != nil {
panic(err)
}
stdout := os.Stdout
os.Stdout = w
f()
os.Stdout = stdout
w.Close()
var buf bytes.Buffer
io.Copy(&buf, r)
return buf.String()
}
os.Pipe()
の行は入出力のパイプで、 w
に書き込まれたものを r
をとおして取得できます。
fmt.Printf
の出力先を一時的に os.Stdout
から w
に変更することで、表示結果を値として取得することができます。
注意すべき点として、このコードはスレッドセーフではないことと、 os.Stderr
は依然として表示されてしまうのでキャプチャしたい場合はこれについても処理する必要があります。
この captureOutput()
を用いると、インテグレーションテストは次のように書けます。
package main
import "testing"
func TestRun(t *testing.T) {
var code int
out := captureStdout(func() {
code = run([]string{"-x", "2", "-y", "3"})
})
if code != ExitCodeOK {
t.Errorf("Unexpected exit code: %d", code)
}
if out != "2 + 3 = 5\n" {
t.Errorf("Unexpected output: %s", out)
}
}
標準出力としてコンソール上に表示されていた文字列が変数 out
に格納されるため、これに対する評価式を書くことでテストとすることができます。
io.Writerで出力先を制御する
はじめに書いた io.Writer
を渡す方法の場合、次のような感じになると思います。
fmt.Fprintf()
に io.Writer
を渡すようにし、 main.go
では os.Stdout
をセットしつつ main_test.go
では別の値を渡すことで、その内容をテストします。
main.go
:
var writer io.Writer
func init() {
writer = os.Stdout
}
// snip
func run(args []string) {
// snip
fprintf("%d + %d = %d", x, y, sum(x, y))
// snip
}
func fprintf(format string, a ...interface{}) {
fmt.Fprintf(writer, format, a...)
}
main_test.go
:
package main
import (
"bytes"
"testing"
)
var buffer *bytes.Buffer
func init() {
buffer = &bytes.Buffer{}
writer = buffer
}
func TestRun(t *testing.T) {
buffer.Reset()
code := run([]string{"-x", "2", "-y", "3"})
if code != ExitCodeOK {
t.Errorf("Unexpected exit code: %d", code)
}
if buffer.String() != "2 + 3 = 5\n" {
t.Errorf("Unexpected output: %s", buffer.String())
}
}
注意すべき点として、 buffer
はクリアしない限り追記されるので、出力内容を検証するテストケースでははじめに buffer.Reset()
でバッファをクリアする必要があります。
おわりに
以上は私が標準出力をテストすることになったときに行った方法ですが、他によいやり方などありましたらぜひコメントなどでご教示いただければと思います