ランダムに抽選する関数の確率テスト(Golang)

自作した社員抽選Slack Botの抽選確率がランダムになっているか調査するテストを書いてみました

> Go + AWS LambdaでSlackの社員抽選botを作った話
https://blog.acall.jp/2019/11/develop-slack-lottery-bot/

## テスト対象関数
```go:main.go
package main

import (
	"math/rand"
	"time"
)


func lotteryOneUserFromUsers(userIDs []string) string {
	rand.Seed(time.Now().UnixNano())
	userID := userIDs[rand.Intn(len(userIDs))]
	return userID
}
```
スライスを渡すと要素の1つがランダムで返ってきます
とてもシンプルな関数です

## テストコード
```go:main_test.go
package main

import (
	"fmt"
	"math"
	"testing"
)

func Test_lotteryOneUserFromUsers(t *testing.T) {
	// initialize
	userIDs := []string{"a", "b", "c", "d", "e"}
	counts := make(map[string]int)
	for _, id := range userIDs {
		counts[id] = 0
	}

	// sampling
	const loopCnt int = 1000
	var i int
	var userID string
	for i < loopCnt {
		userID = lotteryOneUserFromUsers(userIDs)
		counts[userID]++
		i++
	}

	// calculation
	const allowErrRate = 0.05
	var probability float64
	var errRate float64
	expectProbability := float64(1) / float64(len(userIDs))

	fmt.Printf("ID: count, percentage\n")
	for k, v := range counts {
		probability = float64(v) / float64(loopCnt)
		errRate = math.Abs(expectProbability - probability)
		fmt.Printf("%s: %d ", k, v)
		fmt.Printf("%v per \n", probability*100.0)
		if errRate > allowErrRate {
			t.Errorf("Error Rate is too big. Label: %s, Error Rate: %f", k, errRate)
		}
	}
}
```

### 初期化
```go
userIDs := []string{"a", "b", "c", "d", "e"}
counts := make(map[string]int)
for _, id := range userIDs {
	counts[id] = 0
}
```
userIDsに適当な引数定義しています
このuserIDsの抽選を何度も繰り返し、確率を求めます

countsは当選回数を格納するmapです

### サンプリング
```go
const loopCnt int = 1000
var i int
var userID string
for i < loopCnt {
	userID = lotteryOneUserFromUsers(userIDs)
	counts[userID]++
	i++
}
```
loopCntにループ回数を定義し、当選回数をcounts追加するループを回しています

### 確率の計算
```go
const allowErrRate = 0.05
var probability float64
var errRate float64
expectProbability := float64(1) / float64(len(userIDs))

fmt.Printf("ID: count, percentage\n")
for k, v := range counts {
	probability = float64(v) / float64(loopCnt)
	errRate = math.Abs(expectProbability - probability)
	fmt.Printf("%s: %d ", k, v)
	fmt.Printf("%v per \n", probability*100.0)
	if errRate > allowErrRate {
		t.Errorf("Error Rate is too big. Label: %s, Error Rate: %f", k, errRate)
	}
}
```
| 変数 | 説明 |
|:-----------------|-------------------|
| probability | 計算した確率 |
| expectProbability | 理想確率 |
| errRate | 計算した確率誤差 |
| allowErrRate | 許容確率誤差 |

確率の理想値と実際の確率の差を求めて許容確率誤差を超えていないか確認するだけです

## 実行
`go test . -v` でテストを実行してみます
`-v`はテストPASS時にも標準出力を表示するオプションです

```
$ go test . -v
=== RUN   Test_lotteryOneUserFromUsers
ID: count, percentage
a: 202 20.200000000000003 per
b: 205 20.5 per
c: 209 20.9 per
d: 212 21.2 per
e: 172 17.2 per
--- PASS: Test_lotteryOneUserFromUsers (0.01s)
PASS
ok  	lottery	0.021s
```

PASSしてますね
許容確率誤差をもっと小さくしてみると失敗するようになります

```許容誤差0.01の場合
$ go test . -v
=== RUN   Test_lotteryOneUserFromUsers
ID: count
c: 193 19.3 per
d: 219 21.9 per
e: 190 19 per
a: 182 18.2 per
b: 216 21.6 per
--- FAIL: Test_lotteryOneUserFromUsers (0.01s)
    handler_test.go:41: Error Rate is big. Label: d, Error Rate: 0.019000
    handler_test.go:41: Error Rate is big. Label: a, Error Rate: 0.018000
    handler_test.go:41: Error Rate is big. Label: b, Error Rate: 0.016000
FAIL
FAIL	lottery	0.021s
FAIL
```

# まとめ
「このbot確率偏ってるんじゃない？」と聞かれた時はこの結果を見せて正しいロジックであることを証明したいと思います