はじめに
こんにちは、Go界の志村喬です。賢良な皆様におかれましては、ユニットテストでテストケースを書く前にQuickCheckをご利用されていることと存じますがいかがお過ごしでしょうか。
今回はGoでQuickCheckを使う話を書きます。
QuickCheckについて
QuickCheckに関しては僕がここで説明するより検索したほうが早いと思うので詳細は割愛します。雑に言うとブラックボックステストを自動でゴリッとやってくれるHaskell製の便利ツール/ライブラリで、それが便利だということでオープンソースで他言語にも多くポートされました。
- QuickCheck - Wikipedia, the free encyclopedia
- QuviQ homepage
- QuickCheck 勉強してみよう 最初の一歩の前段階 - いたわさににほんしゅ
- Xion/pyqcy · GitHub
- Pythonのだとこれが良いと思います
そもそも僕がGoを使い始めたのはPythonで型がなくてしんどい状況というのがやっぱりあって、その内の1つに型がなくてテストがでかくなるというのがあったからです。だけどPythonのように標準ライブラリで結構なコードが書けるのは好きだったので、そういう意味でGoはちょうどよかったのです。で、型があるならテストケースの生成も自動化しないとあほらしいだろ、という自然な帰結です。
testing/quick
Goの場合は3rd partyライブラリを使わずともテストに関する標準パッケージtestingの下にQuickCheck用のサブパッケージのquickがあって、こいつを使えばQuickCheckっぽいことができます。
テストケース
- main.go
package main import "fmt" func MultipleByThree(x int) int { if x == 3 { // わざとらしいcorner case return 2 } return x*3 } func main() { // 本来であればintの幅を超える場合とかもチェックすべきだけど今回はサンプルなので割愛 fmt.Println(MultipleByThree(-1023568895)) }
- main_test.go
package main import ( "math/rand" "reflect" "testing" "testing/quick" ) const IntRange = 10000 // テスト用の値の幅をいじるために専用の型を定義 type testInt int func randInt(min int , max int) int { return min + rand.Intn(max-min) } // IntRangeの範囲しか返さないようにする func (i testInt) Generate(rand *rand.Rand, size int) reflect.Value { v := testInt(randInt(-1 * IntRange, IntRange)) return reflect.ValueOf(v) } func TestMultipleByThree(t *testing.T) { f := func(i testInt) bool { x := int(i) y := MultipleByThree(x) return y/3 == x && y%3 == 0 } // デフォルトだと100パターンしか試さないので、パターンを1000倍(100,000パターン)に増やしてみる c := &quick.Config{ MaxCountScale: 1000, } if err := quick.Check(f, c); err != nil { t.Error(err) } }
ここでトリッキーなのは、Generatorとして定義するために、わざわざintをラップしたinterfaceを用意しているといこと。自前でstructを用意した場合も、それをラップするテスト用のinterfaceを用意してやるのがきっと一般的なんでしょう。*1
実行結果
ちゃんと3でエラーが出るよ、という結果が出ました。
% go test -test.v === RUN TestMultipleByThree --- FAIL: TestMultipleByThree (0.04 seconds) main_test.go:34: #30980: failed on input 3 FAIL exit status 1 FAIL _/Users/yoshifumi/src/test/qctest/src 0.046s
CheckEqualっていつ使うんですか
f, gという2つの関数を渡して、その関数に同じ引数を与えた時に、同じ結果が返ってくるかどうかを確認する関数。どんなときに使うのかなと同僚やTwitterでやり取りしながら考えてましたが、やはり「既存のきちんと動いているコード」と「そのコードと同じ処理が期待される新しく実装したコード」の比較に使うんでしょうね。たとえば新しいソートアルゴリズムを考えたとか、リファクタリングしている、とか。Goにははじめからプロファイラもついてるので。
実例で言うとたとえばGoの標準パッケージの crypto/subtle のコード。
- src/pkg/crypto/subtle/constant_time.go - The Go Programming Language
- src/pkg/crypto/subtle/constant_time_test.go - The Go Programming Language
たとえばConstantTimeEqという関数があって、この関数は次のようなテストをしています。(上のファイルからコードを抜粋しました。ライセンスはBSDです。)自前でビット演算使って書いた比較関数のほうが速いよ、ってことなんでしょうね、きっと。
// ConstantTimeEq returns 1 if x == y and 0 otherwise. func ConstantTimeEq(x, y int32) int { z := ^(x ^ y) z &= z >> 16 z &= z >> 8 z &= z >> 4 z &= z >> 2 z &= z >> 1 return int(z & 1) } func eq(a, b int32) int { if a == b { return 1 } return 0 } func TestConstantTimeEq(t *testing.T) { err := quick.CheckEqual(ConstantTimeEq, eq, nil) if err != nil { t.Error(err) } }
*1:本来はこの例の関数であればint全体でテストをかけて関数側でエラーを返すようにするのが正しいんだけど、Generateの例として書いたのと簡単のためにこうした。