テスト・フレームワークで Project Euler を解く
別のセクションで「Project Euler で遊ぶ」話を書いたのだが,この記事のコメントで「ユニットテストを書く練習になる」というのがあって「なるほど!」と膝を打った。 つか,自力で気づけよ,自分。 まだまだ TDD が身につかないなぁ。
ちうわけで,以下の問題(Problem)をテスト・フレームワークで解いてみる。
まず,ベタに解いたコード。
//Answer0 returns answer to this problem
func Answer0(max int) int {
sum := 0
for n := 1; n < max; n++ {
if (n%3) == 0 || (n%5) == 0 {
sum += n
}
}
return sum
}
これをテストにかける。
package problem1
import "testing"
func TestAnswer(t *testing.T) {
testCases := []struct {
max, answer int
}{
{max: 10, answer: 23},
}
for _, tc := range testCases {
a := Answer0(tc.max)
if a != tc.answer {
t.Errorf("Answer0(%v) = %v, want %v.", tc.max, a, tc.answer)
}
}
}
実行すると以下の通り。
$ go test -v .
=== RUN TestAnswer
--- PASS: TestAnswer (0.00s)
PASS
ok project-euler/problem-1 0.128s
これで最初の例示について正しい答えを出していることがわかる。 では1,000未満についてもやってみようか。 以下のようにテスト項目を追加する。
func TestAnswer(t * testing.T) {
testCases := []struct {
max, answer int
}{
{max: 10, answer: 23},
{max: 1000, answer: 0},
}
for _ , tc := range testCases {
a := Answer0(tc.max)
if a != tc.answer {
t.Errorf("Answer0(%v) = %v, want %v.", tc.max, a, tc.answer)
}
}
}
まだ正解が分からないので,とりあえず 0
をぶちこんでいる。
これを実行する。
$ go test -v .
=== RUN TestAnswer
--- FAIL: TestAnswer (0.00s)
answer_test.go:16: Answer0(1000) = ******, want 0.
FAIL
FAIL project-euler/problem-1 0.106s
exit status 1
テストは当然失敗するが,回答が表示されるので1 これを Project Euler のサイトで入力してチェックする。 正解が分かったら改めて正解をテストにセットしなおす。
func TestAnswer(t * testing.T) {
testCases := []struct {
max, answer int
}{
{max: 10, answer: 23},
{max: 1000, answer: ******},
}
for _ , tc := range testCases {
a := Answer0(tc.max)
if a != tc.answer {
t.Errorf("Answer0(%v) = %v, want %v.", tc.max, a, tc.answer)
}
}
}
これで再テストすると
$ go test -v .
=== RUN TestAnswer
--- PASS: TestAnswer (0.00s)
PASS
ok project-euler/problem-1 0.128s
ちゃんと PASS していることが確認できた。
改良版
ここで解説を参考に改良版のコードを組む。 こんな感じ。
//Answer1 returns answer to this problem (refactoring version)
func Answer1(max int) int {
return sumDivisibleBy(max, 3) + sumDivisibleBy(max, 5) - sumDivisibleBy(max, 3*5)
}
func sumDivisibleBy(max, n int) int {
m := (max - 1) / n
return n * (m * (m + 1)) / 2
}
このコードも同じテストにかける。
func TestAnswer(t * testing.T) {
testCases := []struct {
max, answer int
}{
{max: 10, answer: 23},
{max: 1000, answer: ******},
}
for _ , tc := range testCases {
a := Answer0(tc.max)
if a != tc.answer {
t.Errorf("Answer0(%v) = %v, want %v.", tc.max, a, tc.answer)
}
a = Answer1(tc.max)
if a != tc.answer {
t.Errorf("Answer1(%v) = %v, want %v.", tc.max, a, tc.answer)
}
}
}
全てのテストケースでパスすればOK。
ベンチマーク・テスト
更に Answer0()
, Answer1()
両関数をベンチマーク・テストで比較してみることにする。
ベンチマーク・テスト用のコードは以下の通り2。
func BenchmarkAnswer0(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Answer0(1000)
}
}
func BenchmarkAnswer1(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Answer1(1000)
}
}
ではベンチマーク・テストを実行してみる。
$ go test -bench . -benchmem
goos: windows
goarch: amd64
pkg: project-euler/problem-1
BenchmarkAnswer0-4 500000 2415 ns/op 0 B/op 0 allocs/op
BenchmarkAnswer1-4 2000000000 0.29 ns/op 0 B/op 0 allocs/op
PASS
ok project-euler/problem-1 1.952s
おおっ。 万倍ちがうぜ(笑)
このようにテストフレームワークを使って楽しく Project Euler で遊ぶことができる。 テストを書く習慣づけになればもっといいんだろうけど。
ブックマーク
参考図書
- プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
- Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)
- 丸善出版 2016-06-20
- 単行本(ソフトカバー)
- 4621300253 (ASIN), 9784621300251 (EAN), 4621300253 (ISBN), 9784621300251 (ISBN)
- 評価
著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。この本は Go 言語の教科書と言ってもいいだろう。