テスト・フレームワークで Project Euler を解く

別のセクションで「Project Euler で遊ぶ」話を書いたのだが,この記事のコメントで「ユニットテストを書く練習になる」というのがあって「なるほど!」と膝を打った。 つか,自力で気づけよ,自分。 まだまだ TDD が身につかないなぁ。

ちうわけで,以下の問題(Problem)をテスト・フレームワークで解いてみる。

If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23.
Find the sum of all the multiples of 3 or 5 below 1000.
10未満の自然数のうち, 3 もしくは 5 の倍数になっているものは 3, 5, 6, 9 の4つがあり, これらの合計は 23 になる.
同じようにして, 1000 未満の 3 か 5 の倍数になっている数字の合計を求めよ.

まず,ベタに解いたコード。

//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 で遊ぶことができる。 テストを書く習慣づけになればもっといいんだろうけど。

ブックマーク

参考図書

photo
プログラミング言語Go
アラン・ドノバン (著), ブライアン・カーニハン (著), 柴田芳樹 (著)
丸善出版 2016-06-20 (Release 2021-07-13)
Kindle版
B099928SJD (ASIN)
評価     

Kindle 版出た! 一部内容が古びてしまったが,この本は Go 言語の教科書と言ってもいいだろう。感想はこちら

reviewed by Spiegel on 2021-05-22 (powered by PA-APIv5)


  1. 一応ネタバレ防止用に伏字にしている。ご容赦。 ↩︎

  2. ベンチマーク用のコードは ***_test.go ファイルに含めること。また関数名は Benchmark から始まる名前にする。 ↩︎