水曜日, 4月 29, 2015

Goのtesting/quickを簡単に触ってみる

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で初期化されているという点だけは要注意ですね。