LoginSignup
3
3

More than 3 years have passed since last update.

golangのテスト機能を使って競プロを快適にテストしてみる

Posted at

はじめに

最近少しずつgolangで競技プログラミングを始めた駆け出しの競技プログラマです。
競プロを組んでると思うのが、プログラムを作って例題でテストして…と進めていくと毎回コンソールで値を入れてチェックというのが面倒くさく感じます。
色々調べた結果、golangのテスト機能を使えば簡単にテストできることがわかったのでテスト用ソースセットをつくってみました!

なお、ソースは以下にあります。
https://github.com/Atoyr/CompetitiveProgramming

実装

※こちらの記事を参考にして作成しました
競プロのテストを「go test」でやりたかったので作った - Qiita
Go言語で標準入出力をスタブする方法 - Qiita

golangのテストの実行については、様々な記事がありますので、本記事では割愛します。

仕組み

テストソースはmain_test.goになります。
このファイルに標準入力と標準出力を記載してgo testを実行するとmain.goのmainをテストしてくれます。
具体的には以下のようになっています。
- 問題の入力を文字列配列で各行ごとに設定
- 問題の出力を文字列で設定
- 問題の入力を標準入力の形でmain関数に渡して、標準出力の結果を生成する
- 標準出力の結果と問題の出力を比較

ね、簡単でしょ

どないなっとんのこれ

このテストで肝となるのは、標準入力、標準出力の扱いになります。
golangには標準入出力をスタブする機能があるので、それを使って結果を取得します。

入出力は以下の関数で実行します。
第一引数に標準入力をスペース区切りで、第二引数のfuncでmain関数を受け取ります。
戻り値は標準出力とエラーメッセージです。
エラーメッセージはエラーが発生していなければ空文字です。

func stubio(inbuf string, f func()) (string, string) {
    inr, inw, _ := os.Pipe()
    outr, outw, _ := os.Pipe()
    errr, errw, _ := os.Pipe()

    orgStdin := os.Stdin
    orgStdout := os.Stdout
    orgStderr := os.Stderr

    inw.Write([]byte(inbuf))
    inw.Close()

    os.Stdin = inr
    os.Stdout = outw
    os.Stderr = errw

    outC := make(chan string)
    errC := make(chan string)
    defer close(outC)
    defer close(errC)
    go func() {
        var buf strings.Builder
        io.Copy(&buf, outr)
        outr.Close()
        outC <- buf.String()
    }()

    go func() {
        var buf strings.Builder
        io.Copy(&buf, errr)
        errr.Close()
        errC <- buf.String()
    }()

    f()

    os.Stdin = orgStdin
    os.Stdout = orgStdout
    os.Stderr = orgStderr
    outw.Close()
    errw.Close()

    return <-outC, <-errC
}

テストする標準入力および標準出力は別途typeを作成して対応しています。
今回はatcoderのチュートリアル1問目の標準入力、標準出力を入れています。
A - Welcome to AtCoder

type testValue struct {
    arg []string
    ans string
}

func getTests() []testValue {
    testValues := []testValue{
        testValue{
            []string{
                "1",
                "2 3",
                "test",
            },
            "6 test"},
        testValue{
            []string{
                "72",
                "128 256",
                "myonmyon",
            },
            "456 myonmyon"},
    }

    return testValues
}

テスト本体は以下になります。
テストデータを取得してmain関数を実行、結果を比較してNGの場合のみエラーメッセージを出します。
これで間違っていた場合でも、mainの実行結果がわかるので対応できます。
また、そもそもmain関数でエラーが発生してもメッセージを表示するようにしています。
ただ、main関数が無限ループになっている場合は結果が帰ってきません。まずいですよ!

func Test_main(t *testing.T) {
    tests := getTests()
    for i, tt := range tests {
        si := strconv.Itoa(i)
        t.Run("Case "+si, func(t *testing.T) {
            ret, err := stubio(strings.Join(tt.arg, " "), main)
            if err != "" {
                t.Errorf("error func: %s ", err)
            }
            ans := fmt.Sprintf("%s\n", tt.ans)
            if ret != ans {
                t.Errorf("Unexpected output: '%s' Need: '%s'", ret, ans)
            }
        })
    }
}

テストを実行できるmain関数を用意しておく

競プロというんだから手早くソースを書きたい気持ちがあります。
なので、毎回使う機能はあらかじめ宣言しておきます。
自分は入力関連の関数をあらかじめ宣言しています。

自分はnextInt()nextString()という関数を用意して、空白区切の文字をint型やstring型で簡単に取り込みできるようにしています。

実際に使ってみる

使う際には、main.goのmain関数内に実際の処理を記載します。
あとは標準入力、標準出力をmain_test.goに記載してテストを実行すればOKです。
テストはコマンドラインやシェルからgo testを実行してください。
gotest1.gif

おまけ

vimを使うとすごく早くテストできます。
必要なプラグインはvim-goになります。
様々な記事で使い方が書いてあるはずなので、ここでは割愛します。

使い方はmain.gomain_test.goのあるディレクトリでvimを開いて、ソースコードを記載。
最後に:GoTestとやってあげればOKです。
gotest2.gif

NGのパターンはぜひ自分で試してみてください。
(gif作ったら容量オーバーでアップできなかったのは内緒。)

これが一番早いと思います。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3