Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

TDD(FizzBuzz) Elm

TDDはリズム感が大事。リズムを覚えていこう。

サイクル1: 3が、"fizz"

最初は関数も用意しないとエラーになるので、ダミーの関数を用意して失敗するケースから始める。

Main.elm

-- 次から省略
module Main exposing (..)

-- 何か関数書かないとエラーになった。
dummy =
    True

Test.elm

-- 次から省略
module Tests exposing (..)

import Test exposing (..)
import TestExp exposing (..)
import Main exposing (..)


--  target modules


all : Test
all =
    describe "FizzBuzzTest"
        [ "3 is fizz"
            => fizzbuzz 3
            === "fizz"
        ]

fizzbuzz関数が無いのでエラー。Red

Cannot find variable `fizzbuzz`

15|             => fizzbuzz 3

fizzbuzz関数を通すようにに書く。

fizzbuzz : Int -> String
fizzbuzz int =
    "fizz"

当然通るので、Green

Running 1 test. To reproduce these results, run: elm-test --fuzz 100 --seed 310762322


TEST RUN PASSED

Duration: 186 ms
Passed:   1
Failed:   0

リファクタリング出来るかを考える。シンプル過ぎて何もしようがない。

サイクル2: 5が、"buzz"

テストコードの追加は「不安が無くなる」までやる。今の状態だと、どんな数字でも"fizz"になってしまう。"buzz"になるケースを入れてRedに持っていく。今回は、5でテストを追加する。

all : Test
all =
    describe "FizzBuzzTest"
        [ "3 is fizz"
            => fizzbuzz 3
            === "fizz"
        , "5 is buzz"
            => fizzbuzz 5
            === "buzz"
        ]

ナイスエラー。Red

↓ Tests
↓ FizzBuzzTest
✗ 5 is buzz

    "buzz"
    ╷
    │ Expect.equal
    ╵
    "fizz"

とりあえずテストが通すことが大事。いきなり解答を書いてしまうのはNG。fizzbuzzの例だから過剰に思えるけど。通常の開発では、テストケースが不足したり過剰になったりするので、テストケースは少しずつ実装も最小限の形でやる。

fizzbuzz : Int -> String
fizzbuzz num =
    if num == 3 then
        "fizz"
    else
        "buzz"

テストが通った。Green

Running 2 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 693332105


TEST RUN PASSED

リファクタリングもまだ良さそうだ。

サイクル3: 6が、"fizz"

サイクルも慣れてきた。テストを失敗させれば良い。そろそろ単純な分岐では誤魔化せなくなってきた。6で"fizz"パターンを行ってみよう。Red

Running 3 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 1249265329

↓ Tests
↓ FizzBuzzTest
✗ 6 is fizz

    "fizz"
    ╷
    │ Expect.equal
    ╵
    "buzz"



TEST RUN FAILED

ここは解答を決めに行って良い。3の倍数が"fizz"であることを実装しよう。Green

fizzbuzz : Int -> String
fizzbuzz num =
    if num % 3 == 0 then
        "fizz"
    else
        "buzz"

リファクタリングもなし。

サイクル4: 1は、"1"

buzzのケースを追加しても良いが、別な道を開こう。1は、"1"になる。

all : Test
all =
    describe "FizzBuzzTest"
        [ "1 is 1"
            => fizzbuzz 1
            === "1"
        , "3 is fizz"
            => fizzbuzz 3
            === "fizz"
        , "6 is fizz"
            => fizzbuzz 6
            === "fizz"
        , "5 is buzz"
            => fizzbuzz 5
            === "buzz"
        ]

1は、buzzではない。Red

Running 4 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 365994767

↓ Tests
↓ FizzBuzzTest
✗ 1 is 1

    "1"
    ╷
    │ Expect.equal
    ╵
    "buzz"



TEST RUN FAILED

5のときbuzzを明示的にして、それ以外のとき数字は文字列化する。

fizzbuzz : Int -> String
fizzbuzz num =
    if num % 3 == 0 then
        "fizz"
    else if num == 5 then
        "buzz"
    else
        toString num

イエス!Green

Running 4 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 1919454353


TEST RUN PASSED

Duration: 182 ms
Passed:   4
Failed:   0

いや、リファクタリングは実コードだけでは無い。テストコードもまた、リファクタリングの対象なのだ。結局のところ、fizzbuzz関数に渡す整数と、出力である文字列の組さえあればテストコードを再現できるので、Caseと呼ぶレコードを宣言し、List.mapでテストケースを表形式で再現する。

type alias Case =
    { num : Int, expected : String }


all : Test
all =
    let
        case2test { num, expected } =
            (toString num ++ " is " ++ expected)
                => fizzbuzz num
                === expected
    in
        describe "FizzBuzzTest" <|
            List.map case2test
                [ Case 1 "1"
                , Case 3 "fizz"
                , Case 6 "fizz"
                , Case 5 "buzz"
                ]

どうやら、結果は良さそうだ。次からテストケースを追加するのは楽になりそうだ。Refactor

Running 4 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 1973920138


TEST RUN PASSED

サイクル5: 10は、"buzz"

TDDにも慣れてきただろう。fizz同様にbuzzにトドメを刺しておこう。

all : Test
all =
    let
        case2test { num, expected } =
            (toString num ++ " is " ++ expected)
                => fizzbuzz num
                === expected
    in
        describe "FizzBuzzTest" <|
            List.map case2test
                [ Case 1 "1"
                , Case 3 "fizz"
                , Case 6 "fizz"
                , Case 5 "buzz"
                , Case 10 "buzz"
                ]

オーケー。テーブルでのテスト追加は正しく機能しているようだ。Red


Running 5 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 1799203444

↓ Tests
↓ FizzBuzzTest
✗ 10 is buzz

    "buzz"
    ╷
    │ Expect.equal
    ╵
    "10"

5の倍数は"buzz"。

fizzbuzz : Int -> String
fizzbuzz num =
    if num % 3 == 0 then
        "fizz"
    else if num % 5 == 0 then
        "buzz"
    else
        toString num

期待通り。Green

Running 5 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 36701260


TEST RUN PASSED

Duration: 185 ms
Passed:   5
Failed:   0

リファクタリングは、なし。

サイクル6: 大ボス。15、"fizzbuzz"

fizzbuzzを入れよう。

all : Test
all =
    let
        case2test { num, expected } =
            (toString num ++ " is " ++ expected)
                => fizzbuzz num
                === expected
    in
        describe "FizzBuzzTest" <|
            List.map case2test
                [ Case 1 "1"
                , Case 3 "fizz"
                , Case 6 "fizz"
                , Case 5 "buzz"
                , Case 10 "buzz"
                , Case 15 "fizzbuzz"
                ]

当然失敗。このテストの良いところは、fizzで失敗ということがわかる。Red

Running 6 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 950604266

↓ Tests
↓ FizzBuzzTest
✗ 15 is fizzbuzz

    "fizzbuzz"
    ╷
    │ Expect.equal
    ╵
    "fizz"



TEST RUN FAILED

ちょっと粒度は荒いが15の倍数が"fizzbuzz"であることを書いてあげよう。先程のテスト結果で、後にfizzbuzzの条件を追記しても、fizzやbuzzの条件に先に引っかかることがわかるので、最初に追加する。

fizzbuzz : Int -> String
fizzbuzz num =
    if num % 15 == 0 then
        "fizzbuzz"
    else if num % 3 == 0 then
        "fizz"
    else if num % 5 == 0 then
        "buzz"
    else
        toString num

よし通った。Green

Running 6 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 1413615285


TEST RUN PASSED

Duration: 195 ms
Passed:   6
Failed:   0

今の状態は、Green。つまりGreenを維持し続ける限り、実装・テストコードを書くことが安全にできる。これがTDDの良いところだ。elmパワーを見せつけよう。 次のコードはテストを通す。Refactor

fizzbuzz : Int -> String
fizzbuzz num =
    case ( num % 3 == 0, num % 5 == 0 ) of
        ( True, True ) ->
            "fizzbuzz"

        ( True, _ ) ->
            "fizz"

        ( _, True ) ->
            "buzz"

        _ ->
            toString num

サイクル7: 仕様との相談

不安が無くなるまでテストを追加したい。ちょっとエッジケースの0を考えてみる。これは、3の倍数でも5の倍数でも15の倍数でもある。そのため0は"0"としたい。テストを書いてみよう。

all : Test
all =
    let
        case2test { num, expected } =
            (toString num ++ " is " ++ expected)
                => fizzbuzz num
                === expected
    in
        describe "FizzBuzzTest" <|
            List.map case2test
                [ Case 0 "0"
                , Case 1 "1"
                , Case 3 "fizz"
                , Case 6 "fizz"
                , Case 5 "buzz"
                , Case 10 "buzz"
                , Case 15 "fizzbuzz"
                ]

0は"fizzbuzz"として見られるので失敗。Red

Running 7 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 466414775

↓ Tests
↓ FizzBuzzTest
✗ 0 is 0

    "0"
    ╷
    │ Expect.equal
    ╵
    "fizzbuzz"



TEST RUN FAILED

Duration: 190 ms
Passed:   6
Failed:   1

あんまりこんな形は書かないと思う。0は、fizzbuzzとしてしまうケースが多そう。これはTDDだから出来た仕様の検討。通常の業務コードでは仕様を深掘りする大事な道具として有効そうであることがわかる。

fizzbuzz : Int -> String
fizzbuzz num =
    if num == 0 then
        "0"
    else
        case ( num % 3 == 0, num % 5 == 0 ) of
            ( True, True ) ->
                "fizzbuzz"

            ( True, _ ) ->
                "fizz"

            ( _, True ) ->
                "buzz"

            _ ->
                toString num

ともあれ、Green

Running 7 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 1201945666


TEST RUN PASSED

Duration: 197 ms
Passed:   7
Failed:   0

リファクタリングは無し。

サイクル8: 気が済むまでテストコードの追加

大丈夫とは思いつつ、不安が無くなるまでテストコードを追加する。テストが通るまでヒヤッとする。これぐらいの不安感がちょうどよいのかもしれない。

all : Test
all =
    let
        case2test { num, expected } =
            (toString num ++ " is " ++ expected)
                => fizzbuzz num
                === expected
    in
        describe "FizzBuzzTest" <|
            List.map case2test
                [ Case -1 "-1"
                , Case 0 "0"
                , Case 1 "1"
                , Case 2 "2"
                , Case -3 "fizz"
                , Case 3 "fizz"
                , Case 6 "fizz"
                , Case -5 "buzz"
                , Case 5 "buzz"
                , Case 10 "buzz"
                , Case -15 "fizzbuzz"
                , Case 15 "fizzbuzz"
                , Case 30 "fizzbuzz"
                ]

飽きるまで追加した!Green


Running 13 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 1983115552


TEST RUN PASSED

Duration: 193 ms
Passed:   13
Failed:   0

ここまでやればfizzbuzz関数は安心して使うことができる。fizzbuzz関数を利用した、fizzbuzzの列を作る関数などをTDDしながら作ると良いだろう。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.