競プロの勉強するときにテストも書く with Go言語
記事一覧はこちら
背景・モチベーション
前半をさっと流し読みだけで止まってしまっている問題解決力を鍛える!アルゴリズムとデータ構造をちゃんと解きながらやろう!と思い、重い腰を上げて取り組み始めました。
始めたはいいものの、入力と出力がそれぞれ標準入力と標準出力だと後から見直したときに見づらいなと思わないでもなかったので、テストを書くようにしました。
本編
要件としてはこちら
- 普通に実行させたときには、標準入力を受け取って標準出力に出す
- テストとして実行させるときには、複数のテストケースを並列で試験させるようにする
本来であれば、テスト対象のコードとテストコードはパッケージを分けたほうが良いのですが、 main
パッケージの関数にパッケージ外からアクセスできないので諦めました。
※ ちゃんと分離させることもできますが面倒なことが増えるので
問題を解くコード
io.Reader
と io.Writer
を受け取る関数を書きます。よく競プロのサンプルとかで、 bufio.NewScanner(os.Stdin)
を見ると思いますが、引数としては io.Reader
を実装している型であれば何でもいいので外側から注入できるようにしておきます。
出力も標準出力に出したいのかバッファに吐きたいかは呼び出し側の都合で変わってくるので、 io.Writer
を引数に取る fmt.Fprintln
を使うようにします。HTTPサーバのサンプル実装とかでよく見る関数ですよね。
// main.go func solve(r io.Reader, w io.Writer) { scanner := bufio.NewScanner(r) /* 省略 */ fmt.Fprintln(w, ans) }
実行時のエントリポイント
先程の関数に標準入力と標準出力を渡してあげればOKです
// main.go func main() { solve(os.Stdin, os.Stdout) }
テストコード
テーブル駆動テストを作成します。 solve
関数に渡すのは下記
- テストケースの値を使った文字列を
bytes.Buffer
に突っ込んだもの- スライスの
[]
をなくすためにstrings.Trim(s string)
を使っています(他にいい方法があったら教えて下さい)
- スライスの
- 出力を受け取る空の
bytes.Buffer
- テストケースで保持する期待する値と比較するときには、
buffer.String()
を使って文字列に変換する
- テストケースで保持する期待する値と比較するときには、
// main_test.go func TestSolve(t *testing.T) { tests := []struct { name string v int a []int want string }{ {"case 1", 2, []int{2, 4, 6, 6, 2, 3}, "4"}, {"case 2", 4, []int{2, 4, 4, 6, 2, 3, 5, 4}, "7"}, } for _, tt := range tests { // shadowing するのを忘れない、でないとループ直後のttで動いてしまう tt := tt // サブテストにして名前つけて後で分かるようにしておく t.Run(tt.name, func(t *testing.T) { t.Parallel() // サブテストを並列で動かすことを許容する fmtSlice := strings.Trim(fmt.Sprintf("%v", tt.a), "[]") // 入力をio.Readerを実装している bytes.Buffer に突っ込む input := bytes.NewBufferString(fmt.Sprintf("%v %v\n%v", len(tt.a), tt.v, fmtSlice)) buffer := &bytes.Buffer{} // 出力先 solve(input, buffer) got := strings.TrimSpace(buffer.String()) if tt.want != got { t.Errorf("want: %v got: %v", tt.want, got) } }) } }
実際にテストを実行させてみます。
$ go test -v === RUN TestSolve === PAUSE TestSolve === CONT TestSolve === RUN TestSolve/case_1 === PAUSE TestSolve/case_1 === RUN TestSolve/case_2 === PAUSE TestSolve/case_2 === CONT TestSolve/case_1 === CONT TestSolve/case_2 --- PASS: TestSolve (0.00s) --- PASS: TestSolve/case_1 (0.00s) --- PASS: TestSolve/case_2 (0.00s) PASS
以上! 本の問題を解いていくぞ!