1クール続けるブログ

とりあえず1クール続けるソフトウェアエンジニアの備忘録

競プロの勉強するときにテストも書く with Go言語

記事一覧はこちら

背景・モチベーション

前半をさっと流し読みだけで止まってしまっている問題解決力を鍛える!アルゴリズムとデータ構造をちゃんと解きながらやろう!と思い、重い腰を上げて取り組み始めました。
始めたはいいものの、入力と出力がそれぞれ標準入力と標準出力だと後から見直したときに見づらいなと思わないでもなかったので、テストを書くようにしました。

本編

要件としてはこちら

  • 普通に実行させたときには、標準入力を受け取って標準出力に出す
  • テストとして実行させるときには、複数のテストケースを並列で試験させるようにする

本来であれば、テスト対象のコードとテストコードはパッケージを分けたほうが良いのですが、 main パッケージの関数にパッケージ外からアクセスできないので諦めました。
※ ちゃんと分離させることもできますが面倒なことが増えるので

問題を解くコード

io.Readerio.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

以上! 本の問題を解いていくぞ!