プログラムを書いていると、不確定要素に依存する関数を書くことは多々あると思います。
不確定要素というのは、以下のようなものです。
- 乱数
- 現在時刻
- 外部I/O (通信・ファイル等)
この記事では、例として乱数に依存する関数を書いてみます。
作ってみるもの
- 0~9の目がある10面ダイスを使う
- 10面ダイスを2つ振った結果で攻撃の判定が行われる
- 攻撃の判定結果は文字列で返す
- 10面ダイスの目が二つとも0
= 00
の場合はクリティカル - 10面ダイスの目が二つとも9
= 99
の場合はファンブル
乱数依存の実装 その1
math/rand
パッケージで毎回異なる結果を得るための手軽な方法として、初期Seedにtime.Now().UnixNano()
を設定します。
RoleD10
で0~9の目が出る10面ダイスを表現しています。
これはもろに乱数に依存しているため、実行のたびに結果が変わります。
package main
import (
"fmt"
"math/rand"
"time"
)
// RNG random number generater
var RNG *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano()))
// RoleD10 returns 0-9
func RoleD10() int {
return RNG.Intn(10)
}
// Attack 攻撃結果は10面ダイスを2つ振ってパーセンテージ判定する
func Attack() (result string) {
p := RoleD10()*10 + RoleD10()
fmt.Println("2D10 Result:", p)
if p == 0 {
return "CRITICAL!!"
}
if p == 99 {
return "FUMBLE..."
}
return "HIT!"
}
func main() {
fmt.Println("Attack Result:", Attack())
}
その1のテスト
このプログラムで実際にレアケースのCRITICAL!!
が表示されるのか確かめようとすると、300回試行しても95%の確率でしかCRITICAL!!
を見ることは出来ないので、テストするのが大変です。
ましてやもっと低確率な事象をプログラムした場合は人力ではテスト出来ないでしょう。
rand
の動きをテストのときだけ任意の値が出るように制御しなければなりません。
乱数依存の実装 その2 (randをインターフェース化)
不確定要素に依存する場合は、その依存部分をインターフェースにするのが常套手段です。
動きを制御したいのは RNG.Intn(10)
の部分なので、これを素直にインターフェースにしてみましょう。
整数値n
を渡すと、0 <= x < n
を返すので、以下のようなインターフェースを定義します。
type RandomNumberGenerator interface {
Intn(n int) int
}
実装は次のように変わります。
具体的な変更ポイントとしては、RNG
の型が *rand.Rand
から、新しく定義したRandomNumberGenerator
インターフェースに変わっています。
その2の実装
rand.New()
が返す*rand.Rand
は Intn(n int) int
を実装しているので、 RandomNumberGenerator
インターフェースも当然満たします。
package main
import (
"fmt"
"math/rand"
"time"
)
// RandomNumberGenerator returns pseudo-random number
type RandomNumberGenerator interface {
Intn(n int) int
}
// RNG random number generator
var RNG RandomNumberGenerator = rand.New(rand.NewSource(time.Now().UnixNano()))
// RoleD10 returns 0-9
func RoleD10() int {
return RNG.Intn(10)
}
// Attack 攻撃結果は10面ダイスを2つ振ってパーセンテージ判定する
func Attack() (result string) {
p := RoleD10()*10 + RoleD10()
fmt.Println("2D10 Result:", p)
if p == 0 {
return "CRITICAL!!"
}
if p == 99 {
return "FUMBLE..."
}
return "HIT!"
}
func main() {
fmt.Println("Attack Result:", Attack())
}
乱数という不確定要素の実装がインターフェース依存になったことで、テストが書けるようになりました。
その2のテスト
テストコードの雛形は gotests サクッと生成してしまいます。
テストのときはパッケージ変数 RNG
にモックを入れて、動作を思い通りに制御します。
package main
import (
"testing"
)
type RandomNumberGeneratorMock struct {
IntnResults []int
}
func (rng *RandomNumberGeneratorMock) Intn(n int) int {
result := rng.IntnResults[0]
rng.IntnResults = rng.IntnResults[1:]
return result
}
// 乱数生成をモックにすることで確実に望みの乱数に対する挙動をテストできる
func TestAttack_RNGのMockで確実に成功(t *testing.T) {
// 1 が出るように仕込む
RNG = &RandomNumberGeneratorMock{IntnResults: []int{0, 1}}
got := Attack()
want := "HIT!"
if got != want {
t.Errorf("Attack() = %v, want %v", got, want)
}
}
// 滅多に起きない確率のケースもテスト可能
func TestAttack_ケース網羅(t *testing.T) {
type fakes struct {
FakeIntnResults []int
}
tests := []struct {
name string
fakes fakes
wantResult string
}{
{"0", fakes{FakeIntnResults: []int{0, 0}}, "CRITICAL!!"},
{"55", fakes{FakeIntnResults: []int{5, 5}}, "HIT!"},
{"99", fakes{FakeIntnResults: []int{9, 9}}, "FUMBLE..."},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// fakes で指定したRNGの結果をここで仕込む
RNG = &RandomNumberGeneratorMock{IntnResults: tt.fakes.FakeIntnResults}
if gotResult := Attack(); gotResult != tt.wantResult {
t.Errorf("Attack() = %v, want %v", gotResult, tt.wantResult)
}
})
}
}
乱数生成器は値を取得するたびに乱数を1つ返すので、その返す乱数が望みのものになるようにint
のスライスで仕込んでおけるようにして、そこから1つずつ取り出すようにします。
Table Driven Testsパターンでは依存する引数をargs
で表すことが多いですが、モックに仕込み通りの動作をさせるための偽物という意味でfakes
と名付けています。
その2の実装の問題
これで乱数という不確定要素に依存する関数のテストを書くという目的は達成できました。
しかし、 math/rand
には Intn(n int) int
以外にも多数のメソッドが定義されているため、この部分をインターフェース化すると、他のメソッドを使いたいときに不便です。
また、Go標準の math/rand
は乱数生成器のアルゴリズムを簡単に入れ替えることができるように、 rand.New
は rand.Source
インターフェースに依存するようになっています。
このおかけで、乱数生成器をメルセンヌ・ツイスタ や xorshift に簡単に入れ替えることが出来ます。
したがってインターフェース化するのは10面ダイスを振る部分 RoleD10() int
にするべきでしょう。
乱数依存の実装 その3 (Diceをインターフェース化)
func Attack() (result string)
が D10Roller
インターフェースに依存するように、 Attacker struct
を新たに定義しています。
依存するものはstruct
のフィールドに取り込んでしまう方が書きやすくなります。
その3の実装
package main
import (
"fmt"
"math/rand"
"time"
)
// D10Roller は 10面ダイスのインターフェース
type D10Roller interface {
RoleD10() int
}
// Dice implements dice role
type Dice struct {
rng *rand.Rand
}
// RoleD10 returns 0-9
func (d *Dice) RoleD10() int {
return d.rng.Intn(10)
}
// Attacker は攻撃役
type Attacker struct {
d10 D10Roller
}
// Attack 攻撃結果は10面ダイスを2つ振ってパーセンテージ判定する
func (a *Attacker) Attack() (result string) {
p := a.d10.RoleD10()*10 + a.d10.RoleD10()
fmt.Println("2D10 Result:", p)
if p == 0 {
return "CRITICAL!!"
}
if p == 99 {
return "FUMBLE..."
}
return "HIT!"
}
func main() {
a := &Attacker{
d10: &Dice{
rng: rand.New(rand.NewSource(time.Now().UnixNano())),
},
}
fmt.Println("Attack Result:", a.Attack())
}
その3のテスト
直接動作を制御したいDiceをモックにしたことで、テストコードはスッキリしています。
package main
import (
"testing"
)
type DiceMock struct {
Results []int
}
func (rng *DiceMock) RoleD10() int {
result := rng.Results[0]
rng.Results = rng.Results[1:]
return result
}
// Diceをモックにすることで確実に望みの結果に対する挙動をテストできる
func TestAttack_DiceのMockで確実に成功(t *testing.T) {
// 1 が出るように仕込む
dice := &DiceMock{Results: []int{0, 1}}
a := &Attacker{d10: dice}
got := a.Attack()
want := "HIT!"
if got != want {
t.Errorf("Attack() = %v, want %v", got, want)
}
}
func TestAttack_ケース網羅(t *testing.T) {
type fakes struct {
FakeResults []int
}
tests := []struct {
name string
fakes fakes
wantResult string
}{
{"0", fakes{FakeResults: []int{0, 0}}, "CRITICAL!!"},
{"55", fakes{FakeResults: []int{5, 5}}, "HIT!"},
{"99", fakes{FakeResults: []int{9, 9}}, "FUMBLE..."},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// fakes で指定したDiceの結果をここで仕込む
dice := &DiceMock{Results: tt.fakes.FakeResults}
a := &Attacker{d10: dice}
if gotResult := a.Attack(); gotResult != tt.wantResult {
t.Errorf("Attack() = %v, want %v", gotResult, tt.wantResult)
}
})
}
}
まとめ
- 不確定要素に依存する関数、メソッドをインターフェースにすることでテスト可能になる
- 外部ライブラリから提供される関数をそのままインターフェースにするより、使いやすい形でラップする方がテストしやすくなる