YAMAGUCHI::weblog

噛み付き地蔵に憧れて、この神の世界にやってきました。マドンナみたいな男の子、コッペです。

GoでQuickCheckをする

はじめに

こんにちは、Go界の志村喬です。賢良な皆様におかれましては、ユニットテストでテストケースを書く前にQuickCheckをご利用されていることと存じますがいかがお過ごしでしょうか。
今回はGoでQuickCheckを使う話を書きます。

QuickCheckについて

QuickCheckに関しては僕がここで説明するより検索したほうが早いと思うので詳細は割愛します。雑に言うとブラックボックステストを自動でゴリッとやってくれるHaskell製の便利ツール/ライブラリで、それが便利だということでオープンソースで他言語にも多くポートされました。

そもそも僕が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 のコード。

たとえば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の例として書いたのと簡単のためにこうした。