Goのpackageを眺めていたら、いまさらtesting/quickというものを見つけたので試してみる。
quickの説明をみると、
help with black box testing
と書いてあるし、Checkという関数があったりするので、QuickCheckのためのパッケージのはず。
普通のtest
まずはquickを使わないテスト。
2つのintで割り算をする関数のテスト。
package main
import "testing"
func div(n, m int) int {
return n / m
}
func TestDiv(t *testing.T) {
if div(4, 2) != 2 {
t.Error("error!")
}
if div(3, 2) != 1 {
t.Error("error!")
}
}
明らかにテストケースが不足してる。
とりあえず第二引数に0を入れたら、確実にエラーになるはずだけど、このケースでは考慮されていない。
quick導入
というわけでquickを導入。
package main
import (
"fmt"
"testing"
"testing/quick"
)
// テスト回数計測のためのカウンター
var count int = 0
func div(n, m int) int {
count++
return n / m
}
func TestDiv(t *testing.T) {
f := func(x, y int) bool {
return div(x, y) == (x / y)
}
if err := quick.Check(f, nil); err != nil {
t.Error(err.Error())
}
fmt.Printf("テスト回数:%d\n", count)
}
これでgo test
してみる。
$ go test
テスト回数:100
PASS
ok quick_example 0.005s
通ってしまった。
quickというかQuickTestは要はとにかくいろいろ試してみるテストなので、100回程度では通ってしまうのも仕方ない。
今回の場合、第二引数に0が入るのは 「1/intの取りうる値」 なので、試行回数が全然足りない。
試行回数を変える
ということで試行回数を増やしてみる。
試行回数の設定をするには、quick.Configのインスタンスを作って、
MaxCountに試行回数を設定する。
package main
import (
"fmt"
"testing"
"testing/quick"
)
var count int = 0
func div(n, m int) int {
count++
return n / m
}
func TestDiv(t *testing.T) {
f := func(x, y int) bool {
return div(x, y) == (x / y)
}
config := &quick.Config{
MaxCount: 10000,
}
if err := quick.Check(f, config); err != nil {
t.Error(err.Error())
}
fmt.Printf("テスト回数:%d\n", count)
}
そしてgo test
する。
$ go test
テスト回数:10000
PASS
ok quick_example 0.015s
通ってしまった。
math.MaxInt64
を突っ込めば確実に失敗するのだろうけど、
それは最早ブラックボックステストではない。
(ちなみにMaxInt64でやってみたら、手元のマシンでは3分経っても終わらなかった)
テスト値を制御する
とはいえ、0ぐらい入れて欲しい。
ブラックボックステストと言っても、本当に無作為に値を突っ込むのではなく、とりあえず0から100と、0から-100で動いてもらえば最低限の仕様は満たすという事はままある。
そのように、自分でランダム性を制御するにはconfig.Valuesに値を生成する関数を設定する。
値を生成する関数はfunc(args []reflect.Value, rand *rand.Rand)
というシグネチャを持つ。
args
には、quick.Check関数の第一引数に渡す関数の引数の数のサイズを持つスライスが渡されてくる。
今回のケースで、0から100の範囲の値を無作為に選ばせるのであれば、こんな感じの関数をconfig.Valuesに渡せば良い。
func(args []reflect.Value, rand *rand.Rand) {
args[0] = reflect.ValueOf(rand.Intn(100))
args[1] = reflect.ValueOf(rand.Intn(100))
}
これで10000回くらい回せば、流石にargs[1]に0が入るはず。
第二引数のrandは見ての通りRandインスタンスが渡ってくる。このRandインスタンスはconfig.Randで設定しないとデフォルトのものが使われるのだけど、go 1.4.2ではデフォルトのrandはseedが0固定されているので、変えないと毎回同じ結果になる(デフォルトのseed=0は意図したものなのか?)。
上記を踏まえるとこんな感じのソースになる。
package main
import (
"math/rand"
"reflect"
"testing"
"testing/quick"
"time"
)
var count int = 0
func div(n, m int) int {
count++
return n / m
}
func TestDiv(t *testing.T) {
f := func(x, y int) bool {
return div(x, y) == (x / y)
}
config := &quick.Config{
MaxCount: 10000,
Rand: rand.New(rand.NewSource(time.Now().UTC().UnixNano())),
Values: func(args []reflect.Value, rand *rand.Rand) {
args[0] = reflect.ValueOf(rand.Intn(100))
args[1] = reflect.ValueOf(rand.Intn(100))
},
}
if err := quick.Check(f, config); err != nil {
t.Error(err.Error())
}
fmt.Printf("テスト回数:%d\n", count)
}
これでgo test
すると、まず確実にこんなかんじで0除算エラーが発生する。めでたしめでたし。
— FAIL: TestDiv (0.00s)
panic: runtime error: integer divide by zero [recovered]
panic: runtime error: integer divide by zero
まとめ
今回はquickを使うというのが目的なので、こんな感じのテストコードになったけど、実際には普通のテスト(=ホワイトボックステスト)と組み合わせて使うはず。
ブラックボックステストはホワイトボックスを置き換えるものではないし。
といっても、特に難しく考えず、とりあえず値を大量に作ってくれる装置として使うだけでも恩恵があるかと。今回のように完全な無作為ではなく、そこそこの無作為にしてグレーボックスとして使ったりとか。
quick固有の問題として、デフォルトのRandがseed=0で初期化されているという点だけは要注意ですね。