はじめに
最近少しずつ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を実行してください。
おまけ
vimを使うとすごく早くテストできます。
必要なプラグインはvim-goになります。
様々な記事で使い方が書いてあるはずなので、ここでは割愛します。
使い方はmain.go
とmain_test.go
のあるディレクトリでvimを開いて、ソースコードを記載。
最後に:GoTest
とやってあげればOKです。
NGのパターンはぜひ自分で試してみてください。
(gif作ったら容量オーバーでアップできなかったのは内緒。)
これが一番早いと思います。