LoginSignup
5
3

More than 3 years have passed since last update.

ビンゴカード作成問題をお題に Elm で TDD の練習1:マスの値を生成する

Last updated at Posted at 2019-06-30

ビンゴカード作成問題 を Elm で TDD しつつ解いた際のアレコレを記録した記事です。
特に 各マスの値の生成 に関する部分についてテストと実装をしています。
UI (表示) については扱っていません。

練習なので TDD 的に変な箇所は多々あるはずです。マサカリ歓迎です。

マスの値生成に関する仕様

  • B、I、N、G、O の 5列が存在する。
  • 各列には 5個のマスが存在する。
  • 各列の各マスに次の範囲の値がランダムに設定される。
    • B列:1-15
    • I列:16-30
    • N列:31-45
    • G列:46-60
    • O列:61-75
  • 同じ値のマスは複数存在しない。
  • 中央のマス (N列の 3段目) 常に FREEマス、それ以外は数字マス 。

準備

シェル上でごにょごにょ
$ mkdir bingo-card-tdd
$ cd bingo-card-tdd
$ elm init
$ elm install elm/random  # 乱数を使うのであらかじめインストール
$ elm-test init

プロダクトコードのモジュール名は Main でメソッド名は bingoCardGenerator にします:

tests/Tests.elm
module Tests exposing (suite)

import Test exposing (..)


suite : Test
suite =
    describe "Main module"
        [ describe "bingoCardGenerator"
            [ todo "テスト書くぞ"
            ]
        ]

動くか試します:

$ elm-test

elm-test 0.19.0-rev6
--------------------

Running 1 test. To reproduce these results, run: elm-test --fuzz 100 --seed 271054124452331 elm-bingo-card-tdd\tests\Tests.elm


TEST RUN INCOMPLETE because there is 1 TODO remaining

Duration: 561 ms
Passed:   0
Failed:   0
Todo:     1
> Tests
> Main module
> bingoCardGenerator
? TODO: テスト書くぞ

動きました。
プロダクトコードのファイルも仮で用意しましょう:

src/Main.elm
module Main exposing (..)


bingoCardGenerator =
    0  -- 仮の戻り値

bingoCardGenerator の仕様とビンゴカードの型

bingoCardGenerator を呼び出したら何が得られればよいでしょうか?
もちろん生成されたビンゴカードの情報です。
ただ、実際にはランダム値 (乱数) を扱うので Random.Generator が登場するはずです。
そうなると型注釈は bingoCardGenerator : Random.Generator ビンゴカードの型 といった形になるでしょう。

ビンゴカードの型を決めないといけません。
5個のマスからなる列が 5個あるので、ひとまず ListList にしましょう:

ビンゴカードの型(仮)
type BingoCard
    = BingoCard (List (List Int))

当初はレコードにしようとして type alias BingoCard = { bingoCard : List (List Int) } と定義したのですが、elm-analyze に「フィールドが 1つしかないレコードだぞ。イケてないぞ」と怒られたのでカスタム型で定義しました。
Elmガイドの記述 に従ったというのにこの仕打ち 1

注意: 一つのフィールドしか無いのになぜレコードにするのか疑問に思う方もいるかも知れません。文字列をそのまま使ってもよいのでは無いでしょうか。もちろんそれでも構いません。しかし最初からレコードにしておくことでアプリケーションが複雑になってもフィールドを追加するのは簡単になります。 2つの テキスト入力をしたくなったとしても、より小さな変更で済むようになります。

それはさておいて。
「マスの値は数値」という前提で型を Int にしましたが、FREEマスがあることを考えると専用の型を定義した方がよさそうです。
ということで用意します:

マスの型
type Cell
    = Num Int  -- 数字マス
    | Free     -- FREEマス

マスの型をビンゴカードの型に反映します:

ビンゴカードの型
type BingoCard
    = BingoCard (List (List Cell))

bingoCardGenerator はこんな感じになります:

bingoCardGenerator
import Random exposing (Generator)


bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    Debug.todo "Generator を返す"

途中経過1 (これまでのコードを反映した *.elm ファイル)
tests/Tests.elm
module Tests exposing (suite)

import Test exposing (..)


suite : Test
suite =
    describe "Main module"
        [ describe "bingoCardGenerator"
            [ todo "テスト書くぞ"
            ]
        ]
src/Main.elm
module Main exposing (..)

import Random exposing (Generator)


type Cell
    = Num Int
    | Free


type BingoCard
    = BingoCard (List (List Cell))


bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    Debug.todo "Generator を返す"

BingoCard の取得

bingoCardGenerator を実行して取得した Generator を直接検証することはできません (多分) 。
Generator から BingoCard を取り出す必要があります。
いい感じの関数がないか Random モジュール を覗くと… Random.step を使えばよさそうです:

Random.step
step : Generator a -> Seed -> ( a, Seed )

Seed はどう用意すれば?と思いましたが、これも同モジュールにある Random.initialSeed でいけそうです:

Random.initialSeed
initialSeed : Int -> Seed

これらを使うとテストコードではこんな感じで BingoCard を取得できそうです:

bingoCardGeneratorを実行してBingoCardを取得
(bingoCard, _) =  -- 戻り値の Seed は無視
    Random.step bingoCardGenerator <| Random.initialSeed x

x はどうやって用意すればよいでしょう?
固定値だと結果も常に同じなので、失敗となるパターンを取りこぼすかもしれません。
あり得るパターンの全網羅は厳しいので、ランダムに抽出した一定数のパターンをテストすることで OK としたいところです。
乱数を作るための乱数が必要になりました :thinking:

ここは Test.fuzzFuzz.int の使いどころ、ということで雰囲気はこんな感じになるでしょう:

Test.fuzzとFuzz.intでテスト用の乱数を取得
import Expect
import Fuzz exposing (Fuzzer)
import Main exposing (..)
import Random
import Test exposing (..)


fuzz Fuzz.int "ビンゴカードテスト(仮)" <|  -- Test.fuzz と Fuzz.int
    \x ->
        let
            (bingoCard, _) =
                Random.step bingoCardGenerator <| Random.initialSeed x
        in
        Expect.fail "bingoCard の検証を実装する"

この辺は @jinjor さん 2 による [Elm] Fuzzer でテストデータを量産しよう が詳しいです。

上記のテストコードでは 100パターンの BingoCard が作成されることになります 3
100パターンなら数としては充分、ということにしましょう。

さてここで「BingoCardFuzzer を使って生成してしまえばいいのでは?:bulb:」と思い至ります。
Fuzz.map の出番です:

import Expect
import Fuzz exposing (Fuzzer)
import Main exposing (..)
import Random
import Test exposing (..)


bingoCardFuzzer : Fuzzer BingoCard
bingoCardFuzzer =
    let
        intToBingoCard : Int -> BingoCard
        intToBingoCard =
            Random.initialSeed >> Random.step bingoCardGenerator >> Tuple.first
    in
    Fuzz.map intToBingoCard Fuzz.int


fuzz bingoCardFuzzer "ビンゴカードテスト(仮)" <|
    \bingoCard ->
        Expect.fail "bingoCard の検証を実装する"

すっきりしました。

途中経過2
tests/Tests.elm
module Tests exposing (suite)

import Expect
import Fuzz exposing (Fuzzer)
import Main exposing (..)
import Random
import Test exposing (..)


bingoCardFuzzer : Fuzzer BingoCard
bingoCardFuzzer =
    let
        intToBingoCard : Int -> BingoCard
        intToBingoCard =
            Random.initialSeed >> Random.step bingoCardGenerator >> Tuple.first
    in
    Fuzz.map intToBingoCard Fuzz.int


suite : Test
suite =
    describe "Main module"
        [ describe "bingoCardGenerator"
            [ fuzz bingoCardFuzzer "ビンゴカードテスト(仮)" <|
                \bingoCard ->
                    Expect.fail "bingoCard の検証を実装する"
            ]
        ]
src/Main.elm
module Main exposing (..)

import Random exposing (Generator)


type Cell
    = Num Int
    | Free


type BingoCard
    = BingoCard (List (List Cell))


bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    Random.constant <| BingoCard []  -- Debug.todo から変更

bingoCardGenerator の実装が Debug.todo のままだと elm-test の実行時に例外が発生してエラーになるので、空のビンゴカードを返す実装に置き換えています。

取得した BingoCard の検証

BingoCard を取得できるようになったのでその中身を検証していきます。
仕様を基に次のことを検証しましょう。

  • ビンゴカードは 5x5 のマスから構成される。
  • B列には 1 以上 15 以下の値が設定される。
  • I列には 16 以上 30 以下の値が設定される。
  • N列には 31 以上 45 以下の値が設定される。
  • G列には 46 以上 60 以下の値が設定される。
  • O列には 61 以上 75 以下の値が設定される。
  • 同じ値のマスは 1個のみ存在する。
  • 中央のマス (N列の 3段目) 常に FREEマス、それ以外は数字マス。

ビンゴカードは 5x5 のマスから構成される

ビンゴカードの実体は List (List Cell) なので、List.length を使用して要素数をカウントすれば OK です。

fuzz bingoCardFuzzer "ビンゴカードは 5x5 のマスで構成される" <|
    \(BingoCard columns) ->
        let
            -- リストの要素数が5個か否かを判定する
            hasOnlyFiveElements : List a -> Bool
            hasOnlyFiveElements aList =
                List.length aList == 5
        in
        Expect.true "ビンゴカードが 5x5 のマスで構成されていません。" <|
            hasOnlyFiveElements columns  -- ビンゴカードに5列のみ存在する
                && List.all hasOnlyFiveElements columns  -- 各列に5マスのみ存在する

Expect.true は第2引数が True だったらテスト OK 、False だったら第1引数のメッセージを出力してテスト NG にする Expectation を返す関数です:

Expect.true
true : String -> Bool -> Expectation

上記のテストを実行すると…

$ elm-test

elm-test 0.19.0-rev6
--------------------

Running 1 test. To reproduce these results, run: elm-test --fuzz 100 --seed 359048069456169 elm-bingo-card-tdd\tests\Tests.elm

> Tests
> Main module
> bingoCardGenerator
> ビンゴカードは 5x5 のマスで構成される

Given BingoCard []

    ビンゴカードが 5x5 のマスで構成されていません。



TEST RUN FAILED

Duration: 601 ms
Passed:   0
Failed:   1

という感じに (bingoCardGenerator の実装が不充分なので) テスト NG になります。
Given BingoCard [] とあるように Test.fuzz の第1引数に与えられた Fuzzer の内容も表示されて便利です。

テストがちゃんと失敗したので仮実装しましょう:

bingoCardGenerator
bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    Random.constant <| BingoCard <| List.repeat 5 <| List.repeat 5 Free

全マスが FREE なビンゴカードを返す仮実装にしてこれをテストすると…

$ elm-test

elm-test 0.19.0-rev6
--------------------

Running 1 test. To reproduce these results, run: elm-test --fuzz 100 --seed 219217117760614 elm-bingo-card-tdd\tests\Tests.elm


TEST RUN PASSED

Duration: 589 ms
Passed:   1
Failed:   0

よしよし。
この程度であればテストコードがバグっていることもそうそうありませんが、念のため 5x5 以外のサイズで NG となるか確認しましょう。

4列しかないビンゴカードを作る実装に変更してテストを実行します:

bingoCardGenerator
bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    Random.constant <| BingoCard <| List.repeat 4 <| List.repeat 5 Free
elm-test実行結果
> ビンゴカードは 5x5 のマスで構成される

Given BingoCard [[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free]]

    ビンゴカードが 5x5 のマスで構成されていません。

ちゃんと NG になりました。
次は各列が 4マスしかないビンゴカードを作る実装に変更してテストを実行します:

bingoCardGenerator
bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    Random.constant <| BingoCard <| List.repeat 5 <| List.repeat 4 Free
elm-test実行結果
> ビンゴカードは 5x5 のマスで構成される

Given BingoCard [[Free,Free,Free,Free],[Free,Free,Free,Free],[Free,Free,Free,Free],[Free,Free,Free,Free],[Free,Free,Free,Free]]

    ビンゴカードが 5x5 のマスで構成されていません。

こちらも NG になりました。
コードは割愛しますが 6マスx5列や 5マスx6列の場合も NG になりました:

elm-test実行結果(6マスx5列)
> ビンゴカードは 5x5 のマスで構成される

Given BingoCard [[Free,Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free,Free]]

    ビンゴカードが 5x5 のマスで構成されていません。
elm-test実行結果(5マスx6列)
> ビンゴカードは 5x5 のマスで構成される

Given BingoCard [[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free]]

    ビンゴカードが 5x5 のマスで構成されていません。

これで「ビンゴカードが 5x5 のマスから構成される」ことを検証するテストコードは OK としましょう。

途中経過3
tests/Tests.elm
module Tests exposing (suite)

import Expect
import Fuzz exposing (Fuzzer)
import Main exposing (..)
import Random
import Test exposing (..)


bingoCardFuzzer : Fuzzer BingoCard
bingoCardFuzzer =
    let
        intToBingoCard : Int -> BingoCard
        intToBingoCard =
            Random.initialSeed >> Random.step bingoCardGenerator >> Tuple.first
    in
    Fuzz.map intToBingoCard Fuzz.int


suite : Test
suite =
    describe "Main module"
        [ describe "bingoCardGenerator"
            [ fuzz bingoCardFuzzer "ビンゴカードは 5x5 のマスで構成される" <|
                \(BingoCard columns) ->
                    let
                        hasOnlyFiveElements : List a -> Bool
                        hasOnlyFiveElements aList =
                            List.length aList == 5
                    in
                    Expect.true "ビンゴカードが 5x5 のマスで構成されていません。" <|
                        hasOnlyFiveElements columns
                            && List.all hasOnlyFiveElements columns
            ]
        ]
src/Main.elm
module Main exposing (BingoCard(..), Cell(..), bingoCardGenerator)

import Random exposing (Generator)


type Cell
    = Num Int
    | Free


type BingoCard
    = BingoCard (List (List Cell))


bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    Random.constant <| BingoCard <| List.repeat 5 <| List.repeat 5 Free

B列には 1 以上 15 以下の値が設定される

B列の各マスについて 1 以上 15 以下の値が設定されているかを検証します。
ビンゴカードから 1番目の列を取得し、その列の全ての値が範囲内であれば OK 、範囲外の値が存在すれば NG とします。

1番目の列を取得するため elm-community/list-extra パッケージの List.Extra.getAt を使います:

List.Exstra.getAt
getAt : Int -> List a -> Maybe a

List.Extra モジュールを使用するために elm-community/list-extra パッケージをインストールします:

シェル上でごにょごにょ
$ elm-test install elm-community/list-extra  # テストコードで必要なので elm-test でインストール

ビンゴカードの値は先のテストと同じく (BingoCard columns) の形で取得することにします。
それを踏まえると、テストコードは次のようになります:

B列の値を検証するテストコード
case ListEx.getAt 0 columns of
    Just [ Num n1, Num n2, Num n3, Num n4, Num n5 ] ->
        [ n1, n2, n3, n4, n5 ]                  -- マスが 5個存在し、数字マスが 5個存在する
            |> List.all (\n -> 1 <= n && n <= 15)
            |> Expect.true "範囲外の値があります。"

    Just [ _, _, _, _, _ ] ->
        Expect.fail "FREEマスが存在します。"      -- マスが 5個存在し、数字マスが 5個未満 == FREEマスが存在する

    Just _ ->
        Expect.fail "マスの数が 5 個ではありません。"

    Nothing ->
        Expect.fail "検証対象の列が存在しません。"  -- 1番目の列が存在しない

5個のマス全てが数字マスである場合には、値が全て範囲内かを判定して OK/NG にします。
FREEマスが存在する場合 or 列のマス数に過不足がある場合 or 1番目の列がそもそも存在しない場合、NG にします。
列のマス数の過不足については先のテストで検証していますが、パターンマッチの都合上ここでも対応します。

テストコードをテストするべく elm-test を実行…する前に、bingoCardGenerator をテスト用に変更します:

bingoCardGenerator
bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    Random.constant <| BingoCard <| 
        [ [ Num 1, Num 2, Num 3, Num 14, Num 15 ]
        , [ Free, Free, Free, Free, Free ]
        , [ Free, Free, Free, Free, Free ]
        , [ Free, Free, Free, Free, Free ]
        , [ Free, Free, Free, Free, Free ]
        ]

1列目だけ数字マスにし、その値を 1列目の範囲内のものにしておきます。
これはテスト OK のはずです。

elm-test実行結果
TEST RUN PASSED

よし。
後は NG となる値を設定して実際に NG になるか確認します。

下限値より小さい値がある

bingoCardGenerator
bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    Random.constant <| BingoCard <| 
        [ [ Num 0, Num 2, Num 3, Num 14, Num 15 ]  -- 1個目が 0 (< 1)
        , [ Free, Free, Free, Free, Free ]
        , [ Free, Free, Free, Free, Free ]
        , [ Free, Free, Free, Free, Free ]
        , [ Free, Free, Free, Free, Free ]
        ]
elm-test実行結果
> B列には 1 以上 15 以下の値が設定される

Given BingoCard [[Num 0,Num 2,Num 3,Num 14,Num 15],[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free]]

    範囲外の値があります。

上限値より大きい値がある

bingoCardGenerator
bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    Random.constant <| BingoCard <| 
        [ [ Num 1, Num 16, Num 3, Num 14, Num 15 ]  -- 2個目が 16 (> 15)
        , [ Free, Free, Free, Free, Free ]
        , [ Free, Free, Free, Free, Free ]
        , [ Free, Free, Free, Free, Free ]
        , [ Free, Free, Free, Free, Free ]
        ]
elm-test実行結果
> B列には 1 以上 15 以下の値が設定される

Given BingoCard [[Num 1,Num 16,Num 3,Num 14,Num 15],[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free]]

    範囲外の値があります。

FREEマスがある

bingoCardGenerator
bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    Random.constant <| BingoCard <| 
        [ [ Num 1, Num 2, Free, Num 14, Num 15 ]  -- 3個目が FREEマス
        , [ Free, Free, Free, Free, Free ]
        , [ Free, Free, Free, Free, Free ]
        , [ Free, Free, Free, Free, Free ]
        , [ Free, Free, Free, Free, Free ]
        ]
elm-test実行結果
> B列には 1 以上 15 以下の値が設定される

Given BingoCard [[Num 1,Num 2,Free,Num 14,Num 15],[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free]]

    FREEマスが存在します。

マスの数が少ない

bingoCardGenerator
bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    Random.constant <| BingoCard <| 
        [ [ Num 1, Num 2, Num 14, Num 15 ]  -- マスの数が 4個
        , [ Free, Free, Free, Free, Free ]
        , [ Free, Free, Free, Free, Free ]
        , [ Free, Free, Free, Free, Free ]
        , [ Free, Free, Free, Free, Free ]
        ]
elm-test実行結果
> B列には 1 以上 15 以下の値が設定される

Given BingoCard [[Num 1,Num 2,Num 14,Num 15],[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free]]

    マスの数が 5 個ではありません。

マスの数が多い

bingoCardGenerator
bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    Random.constant <| BingoCard <| 
        [ [ Num 1, Num 2, Num 3, Num 13, Num 14, Num 15 ]  -- マスの数が 6個
        , [ Free, Free, Free, Free, Free ]
        , [ Free, Free, Free, Free, Free ]
        , [ Free, Free, Free, Free, Free ]
        , [ Free, Free, Free, Free, Free ]
        ]
elm-test実行結果
> B列には 1 以上 15 以下の値が設定される

Given BingoCard [[Num 1,Num 2,Num 3,Num 13,Num 14,Num 15],[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free],[Free,Free,Free,Free,Free]]

    マスの数が 5 個ではありません。

列がない

bingoCardGenerator
bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    Random.constant <| BingoCard <| []  -- 1番目の列がない == 全ての列がない
elm-test実行結果
> B列には 1 以上 15 以下の値が設定される

Given BingoCard []

    検証対象の列が存在しません。

細かい網羅はできていませんが、パターンマッチによる分岐は確認できたのでテストコードのテストはとりあえず OK としましょう。

bingoCardGenerator の実装を進めていきます。
各列のマスは 15種の数字からランダムに選択した 5個 (N列は 4個+ FREEマス) を割り当てたものですが、コード上は 15種の数字を持つリストを用意し、その要素をランダムに入れ替えて (シャッフルして) リスト先頭から 5個を取り出す という形で実装します。

リスト要素をシャッフルするための関数は elm-community/random-extra パッケージに Random.List.shuffle が用意されています:

Random.List.shuffle
shuffle : List a -> Generator (List a)

Random.List モジュールを使用するために elm-community/random-extra パッケージをインストールします:

シェル上でごにょごにょ
$ elm install elm-community/random-extra

まずビンゴカード 1列分の値を生成するための Gnerator を作成します:

1列分の値を生成するGenerator
import Random.List as RandomList

toColumnGenerator : List Int -> Generator (List Cell)
toColumnGenerator ints =
    ints
        |> List.map Num              -- Cell 型に変換
        |> RandomList.shuffle        -- リストの要素をランダムにシャッフル (するための Generator を作成)
        |> Random.map (List.take 5)  -- リストの先頭から 5個を取り出す

toColumnGenerator は上限下限を受け取って内部で List.range を呼び出す形でもよいですが、今回は List を受け取るようにしました。

次に上記の Generator を使ってビンゴカードを生成するための Generator を作成します。
今注目しているのは 1列目の値だけなので、ここでは 1列目だけをランダムに作成する実装とします:

ビンゴカードを生成するGenerator
import Random.Extra as RandomEx

bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    let
        toColumnGenerator : List Int -> Generator (List Cell)
        :
    in
    [ List.range 1 15 |> toColumnGenerator
    , [ Free, Free, Free, Free, Free ] |> Random.constant
    , [ Free, Free, Free, Free, Free ] |> Random.constant
    , [ Free, Free, Free, Free, Free ] |> Random.constant
    , [ Free, Free, Free, Free, Free ] |> Random.constant
    ]
        |> RandomEx.sequence     -- List (Generator (List Cell)) を Generator (List (List Cell)) に変換
        |> Random.map BingoCard  -- Generator (List (List Cell)) を Generator BingoCard に変換

Random.Extra.sequenceList (Generator a)Generator (List a) に変換する関数です 4

Random.Extra.sequence
sequence : List (Generator a) -> Generator (List a)

bingoCardGenerator の実装ができたのでテストします:

$ elm-test

elm-test 0.19.0-rev6
--------------------

Running 2 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 388631187619477 elm-bingo-card-tdd\tests\Tests.elm


TEST RUN PASSED

Duration: 888 ms
Passed:   2
Failed:   0

OK です。

途中経過4
tests/Tests.elm
module Tests exposing (suite)

import Expect exposing (Expectation)
import Fuzz exposing (Fuzzer)
import List.Extra as ListEx
import Main exposing (..)
import Random
import Test exposing (..)


bingoCardFuzzer : Fuzzer BingoCard
bingoCardFuzzer =
    let
        intToBingoCard : Int -> BingoCard
        intToBingoCard =
            Random.initialSeed >> Random.step bingoCardGenerator >> Tuple.first
    in
    Fuzz.map intToBingoCard Fuzz.int


suite : Test
suite =
    describe "Main module"
        [ describe "bingoCardGenerator"
            [ fuzz bingoCardFuzzer "ビンゴカードは 5x5 のマスで構成される" <|
                \(BingoCard columns) ->
                    let
                        hasOnlyFiveElements : List a -> Bool
                        hasOnlyFiveElements aList =
                            List.length aList == 5
                    in
                    Expect.true "ビンゴカードが 5x5 のマスで構成されていません。" <|
                        hasOnlyFiveElements columns
                            && List.all hasOnlyFiveElements columns
            , fuzz bingoCardFuzzer "B列には 1 以上 15 以下の値が設定される" <|
                \(BingoCard columns) ->
                    case ListEx.getAt 0 columns of
                        Just [ Num n1, Num n2, Num n3, Num n4, Num n5 ] ->
                            [ n1, n2, n3, n4, n5 ]
                                |> List.all (\n -> 1 <= n && n <= 15)
                                |> Expect.true "範囲外の値があります。"

                        Just [ _, _, _, _, _ ] ->
                            Expect.fail "FREEマスが存在します。"

                        Just _ ->
                            Expect.fail "マスの数が 5 個ではありません。"

                        Nothing ->
                            Expect.fail "検証対象の列が存在しません。"
            ]
        ]
src/Main.elm
module Main exposing (BingoCard(..), Cell(..), bingoCardGenerator)

import Random exposing (Generator)
import Random.Extra as RandomEx
import Random.List as RandomList


type Cell
    = Num Int
    | Free


type BingoCard
    = BingoCard (List (List Cell))


bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    let
        toColumnGenerator : List Int -> Generator (List Cell)
        toColumnGenerator ints =
            ints
                |> List.map Num
                |> RandomList.shuffle
                |> Random.map (List.take 5)
    in
    [ List.range 1 15 |> toColumnGenerator
    , [ Free, Free, Free, Free, Free ] |> Random.constant
    , [ Free, Free, Free, Free, Free ] |> Random.constant
    , [ Free, Free, Free, Free, Free ] |> Random.constant
    , [ Free, Free, Free, Free, Free ] |> Random.constant
    ]
        |> RandomEx.sequence
        |> Random.map BingoCard

I列には 16 以上 30 以下の値が設定される

B列と同様に I列のテストコードも書きます:

I列の値を検証するテストコード
fuzz bingoCardFuzzer "I列には 16 以上 30 以下の値が設定される" <|
    \(BingoCard columns) ->
        case ListEx.getAt 1 columns of
            Just [ Num n1, Num n2, Num n3, Num n4, Num n5 ] ->
                [ n1, n2, n3, n4, n5 ]
                    |> List.all (\n -> 16 <= n && n <= 30)
                    |> Expect.true "範囲外の値があります。"

            Just [ _, _, _, _, _ ] ->
                Expect.fail "FREEマスが存在します。"

            Just _ ->
                Expect.fail "マスの数が 5 個ではありません。"

            Nothing ->
                Expect.fail "検証対象の列が存在しません。"

B列のテストと同じようななんやかんやを経てプロダクトコードも実装します:

bingoCardGenerator
bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    let
        toColumnGenerator : List Int -> Generator (List Cell)
        toColumnGenerator ints =
            ints
                |> List.map Num
                |> RandomList.shuffle
                |> Random.map (List.take 5)
    in
    [ List.range 1 15 |> toColumnGenerator
    , List.range 16 30 |> toColumnGenerator
    , [ Free, Free, Free, Free, Free ] |> Random.constant
    , [ Free, Free, Free, Free, Free ] |> Random.constant
    , [ Free, Free, Free, Free, Free ] |> Random.constant
    ]
        |> RandomEx.sequence
        |> Random.map BingoCard

テストします:

$ elm-test

elm-test 0.19.0-rev6
--------------------

Running 3 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 72026048068376 elm-bingo-card-tdd\tests\Tests.elm


TEST RUN PASSED

Duration: 910 ms
Passed:   3
Failed:   0

OK 、となったところでテストコードに目を向けましょう。
B列のテストコードと I列のテストコードはほぼ同じで、異なるのは「どの列か」「下限」「上限」の 3点のみです。

B列のテストコードとI列のテストコードの差分
- fuzz bingoCardFuzzer "B列には 1 以上 15 以下の値が設定される" <|
+ fuzz bingoCardFuzzer "I列には 16 以上 30 以下の値が設定される" <|
      \(BingoCard columns) ->
-         case ListEx.getAt 0 columns of
+         case ListEx.getAt 1 columns of
              Just [ Num n1, Num n2, Num n3, Num n4, Num n5 ] ->
                  [ n1, n2, n3, n4, n5 ]
-                     |> List.all (\n -> 1 <= n && n <= 15)
+                     |> List.all (\n -> 16 <= n && n <= 30)
                      |> Expect.true "範囲外の値があります。"

              Just [ _, _, _, _, _ ] ->
                  Expect.fail "FREEマスが存在します。"

              Just _ ->
                  Expect.fail "マスの数が 5 個ではありません。"

              Nothing ->
                  Expect.fail "検証対象の列が存在しません。"

他の列についても同じようなテストコードになるのは明白なので、ここで共通部分はまとめておくことにします:

列のテストを関数にまとめる
verifyColumn : String -> Int -> Int -> Int -> Test
verifyColumn title index min max =
    fuzz bingoCardFuzzer title <|
        \(BingoCard columns) ->
            case ListEx.getAt index columns of
                Just [ Num n1, Num n2, Num n3, Num n4, Num n5 ] ->
                    [ n1, n2, n3, n4, n5 ]
                        |> List.all (\n -> min <= n && n <= max)
                        |> Expect.true "範囲外の値があります。"

                Just [ _, _, _, _, _ ] ->
                    Expect.fail "FREEマスが存在します。"

                Just _ ->
                    Expect.fail "マスの数が 5 個ではありません。"

                Nothing ->
                    Expect.fail "検証対象の列が存在しません。"


suite : Test
suite =
    describe "Main module"
        [ describe "bingoCardGenerator"
            [ fuzz bingoCardFuzzer "ビンゴカードは 5x5 のマスで構成される" <|
              :
            , verifyColumn "B列には 1 以上 15 以下の値が設定される" 0 1 15
            , verifyColumn "I列には 16 以上 30 以下の値が設定される" 1 16 30
            ]
        ]

verifyColumntitle も他の引数から決定するようにしてもよいですが、あまりごちゃごちゃさせたくないので上記のコードを採用します。

テストコードを修正したらテストを実行します:

$ elm-test

elm-test 0.19.0-rev6
--------------------

Running 3 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 128565108805008 elm-bingo-card-tdd\tests\Tests.elm


TEST RUN PASSED

Duration: 1029 ms
Passed:   3
Failed:   0

OK です。

途中経過5
tests/Tests.elm
module Tests exposing (suite)

import Expect exposing (Expectation)
import Fuzz exposing (Fuzzer)
import List.Extra as ListEx
import Main exposing (..)
import Random
import Test exposing (..)


bingoCardFuzzer : Fuzzer BingoCard
bingoCardFuzzer =
    let
        intToBingoCard : Int -> BingoCard
        intToBingoCard =
            Random.initialSeed >> Random.step bingoCardGenerator >> Tuple.first
    in
    Fuzz.map intToBingoCard Fuzz.int


verifyColumn : String -> Int -> Int -> Int -> Test
verifyColumn title index min max =
    fuzz bingoCardFuzzer title <|
        \(BingoCard columns) ->
            case ListEx.getAt index columns of
                Just [ Num n1, Num n2, Num n3, Num n4, Num n5 ] ->
                    [ n1, n2, n3, n4, n5 ]
                        |> List.all (\n -> min <= n && n <= max)
                        |> Expect.true "範囲外の値があります。"

                Just [ _, _, _, _, _ ] ->
                    Expect.fail "FREEマスが存在します。"

                Just _ ->
                    Expect.fail "マスの数が 5 個ではありません。"

                Nothing ->
                    Expect.fail "検証対象の列が存在しません。"


suite : Test
suite =
    describe "Main module"
        [ describe "bingoCardGenerator"
            [ fuzz bingoCardFuzzer "ビンゴカードは 5x5 のマスで構成される" <|
                \(BingoCard columns) ->
                    let
                        hasOnlyFiveElements : List a -> Bool
                        hasOnlyFiveElements aList =
                            List.length aList == 5
                    in
                    Expect.true "ビンゴカードが 5x5 のマスで構成されていません。" <|
                        hasOnlyFiveElements columns
                            && List.all hasOnlyFiveElements columns
            , verifyColumn "B列には 1 以上 15 以下の値が設定される" 0 1 15
            , verifyColumn "I列には 16 以上 30 以下の値が設定される" 1 16 30
            ]
        ]
src/Main.elm
module Main exposing (BingoCard(..), Cell(..), bingoCardGenerator)

import Random exposing (Generator)
import Random.Extra as RandomEx
import Random.List as RandomList


type Cell
    = Num Int
    | Free


type BingoCard
    = BingoCard (List (List Cell))


bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    let
        toColumnGenerator : List Int -> Generator (List Cell)
        toColumnGenerator ints =
            ints
                |> List.map Num
                |> RandomList.shuffle
                |> Random.map (List.take 5)
    in
    [ List.range 1 15 |> toColumnGenerator
    , List.range 16 30 |> toColumnGenerator
    , [ Free, Free, Free, Free, Free ] |> Random.constant
    , [ Free, Free, Free, Free, Free ] |> Random.constant
    , [ Free, Free, Free, Free, Free ] |> Random.constant
    ]
        |> RandomEx.sequence
        |> Random.map BingoCard

N列には 31 以上 45 以下の値が設定される

テストコードの共通部分を verifyColumn にまとめたので残る N~O列のテストは楽勝ですな。
とはいきません。
N列は他の列と仕様が異なり、3番目のマスは固定で FREEマスとなっているため verifyColumn を使うことはできないのです。
N列については専用のテストコードを用意します:

fuzz bingoCardFuzzer "I列には 31 以上 45 以下の値が設定される" <|
    \(BingoCard columns) ->
        case ListEx.getAt 2 columns of
            Just [ Num n1, Num n2, Free, Num n4, Num n5 ] ->       -- 3番目は FREEマス
                [ n1, n2, n4, n5 ]
                    |> List.all (\n -> 31 <= n && n <= 45)
                    |> Expect.true "範囲外の値があります。"

            Just [ _, _, Num n3, _, _ ] ->
                Expect.fail "FREEマスの位置に数字マスが存在します。"  -- 3番目に数字マスがあるのは NG

            Just [ _, _, _, _, _ ] ->
                Expect.fail "数字マスの位置にFREEマスが存在します。"

            Just _ ->
                Expect.fail "マスの数が 5 個ではありません。"

            Nothing ->
                Expect.fail "検証対象の列が存在しません。"

これまでと同様にテストコードのテストはなんやかんやで終わらせ、プロダクトコードの実装をしていきます。
3番目のマスを FREEマスにするため、列の値を生成後に 3番目を Free に置き換える処理を追加します。
List の任意の要素を置き換えるには List.Extra.setAt を使用します:

List.Exstra.setAt
setAt : Int -> a -> List a -> List a

現時点で elm-community/list-extra パッケージはテスト用にインストールしていますが、プロダクトコード用にはインストールしていません (elm-test install でインストールしたため) 。
なので、改めて elm install でインストールします:

$ elm install elm-community/list-extra
I found it in your elm.json file, but in the "test-dependencies" field.
Should I move it into "dependencies" for more general use? [Y/n]:

「elm-community/list-extra なら test-dependencies にあるのを見付けたけど dependencies に移動させる?」とのこと。
Yes なのでそのままエンターして続行します:

-- INVALID PACKAGE DEPENDENCIES --------------------------------------- elm.json

The dependencies in your elm.json are not compatible.

Did you change them by hand? Try to change it back! It is much better to add
dependencies with elm install or the dependency management tool in elm reactor.

Please ask for help on the Elm slack <http://elmlang.herokuapp.com/> if you try
those paths and still cannot figure it out!

「おめーの elm.json にある依存パッケージの記述、互換性ねーから。手でいじったやろ? 元に戻して」とのこと。
なんだと。

elm.json
{
    :
    "dependencies": {
        "direct": {
            "elm/browser": "1.0.1",
            "elm/core": "1.0.2",
            :
        },
        "indirect": {
            :
        }
    },
    "test-dependencies": {
        "direct": {
            "elm-explorations/test": "1.2.1",
            "elm-community/list-extra": "8.2.0"
        },
        "indirect": {}
    }
}

elm コマンドからすれば elm-tset コマンドは知らない子なので、そちらのフォーマットをサポートしろと言われても困るんでしょうけど…。
でもパースはできてるのに (test-dependencies にあるのを見付けているので) 。移動はできないという。えぇ…。

しょうがないので test-dependencies にある elm-community/list-extra の行を削除し、再度 elm install を実施しました:

$ elm install elm-community/list-extra
Here is my plan:

  Add:
    elm-community/list-extra    8.2.0

Would you like me to update your elm.json accordingly? [Y/n]:
Dependencies loaded from local cache.
Dependencies ready!
elm.json
{
    :
    "dependencies": {
        "direct": {
            "elm/browser": "1.0.1",
            "elm/core": "1.0.2",
            :
            "elm-community/list-extra": "8.2.0",
            :
        },
        "indirect": {
            :
        }
    },
    "test-dependencies": {
        "direct": {
            "elm-explorations/test": "1.2.1"
        },
        "indirect": {}
    }
}

テストコードの依存パッケージでも elm install でインストールする方が無難なのかもしれません。
ともあれこれで List.Extra モジュールの関数を使えるようになりました。
bingoCardGenerator の実装を進めていきます:

bingoCardGenerator
import List.Extra as ListEx

bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    let
        toColumnGenerator : List Int -> Generator (List Cell)
        toColumnGenerator ints =
            ints
                |> List.map Num
                |> RandomList.shuffle
                |> Random.map (List.take 5)
    in
    [ List.range 1 15 |> toColumnGenerator
    , List.range 16 30 |> toColumnGenerator
    , List.range 31 45 |> toColumnGenerator
        |> Random.map (ListEx.setAt 2 Free)  -- 3番目の要素 (添え字は 2) を Free に変更
    , [ Free, Free, Free, Free, Free ] |> Random.constant
    , [ Free, Free, Free, Free, Free ] |> Random.constant
    ]
        |> RandomEx.sequence
        |> Random.map BingoCard

あとは elm-test を実行します:

$ elm-test

elm-test 0.19.0-rev6
--------------------

Running 4 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 77513012609946 elm-bingo-card-tdd\tests\Tests.elm


TEST RUN PASSED

Duration: 1029 ms
Passed:   4
Failed:   0

よし。

途中経過6
tests/Tests.elm
module Tests exposing (suite)

import Expect exposing (Expectation)
import Fuzz exposing (Fuzzer)
import List.Extra as ListEx
import Main exposing (..)
import Random
import Test exposing (..)


bingoCardFuzzer : Fuzzer BingoCard
bingoCardFuzzer =
    let
        intToBingoCard : Int -> BingoCard
        intToBingoCard =
            Random.initialSeed >> Random.step bingoCardGenerator >> Tuple.first
    in
    Fuzz.map intToBingoCard Fuzz.int


verifyColumn : String -> Int -> Int -> Int -> Test
verifyColumn title index min max =
    fuzz bingoCardFuzzer title <|
        \(BingoCard columns) ->
            case ListEx.getAt index columns of
                Just [ Num n1, Num n2, Num n3, Num n4, Num n5 ] ->
                    [ n1, n2, n3, n4, n5 ]
                        |> List.all (\n -> min <= n && n <= max)
                        |> Expect.true "範囲外の値があります。"

                Just [ _, _, _, _, _ ] ->
                    Expect.fail "FREEマスが存在します。"

                Just _ ->
                    Expect.fail "マスの数が 5 個ではありません。"

                Nothing ->
                    Expect.fail "検証対象の列が存在しません。"


suite : Test
suite =
    describe "Main module"
        [ describe "bingoCardGenerator"
            [ fuzz bingoCardFuzzer "ビンゴカードは 5x5 のマスで構成される" <|
                \(BingoCard columns) ->
                    let
                        hasOnlyFiveElements : List a -> Bool
                        hasOnlyFiveElements aList =
                            List.length aList == 5
                    in
                    Expect.true "ビンゴカードが 5x5 のマスで構成されていません。" <|
                        hasOnlyFiveElements columns
                            && List.all hasOnlyFiveElements columns
            , verifyColumn "B列には 1 以上 15 以下の値が設定される" 0 1 15
            , verifyColumn "I列には 16 以上 30 以下の値が設定される" 1 16 30
            , fuzz bingoCardFuzzer "I列には 31 以上 45 以下の値が設定される" <|
                \(BingoCard columns) ->
                    case ListEx.getAt 2 columns of
                        Just [ Num n1, Num n2, Free, Num n4, Num n5 ] ->
                            [ n1, n2, n4, n5 ]
                                |> List.all (\n -> 31 <= n && n <= 45)
                                |> Expect.true "範囲外の値があります。"

                        Just [ _, _, Num n3, _, _ ] ->
                            Expect.fail "FREEマスの位置に数字マスが存在します。"

                        Just [ _, _, _, _, _ ] ->
                            Expect.fail "数字マスの位置にFREEマスが存在します。"

                        Just _ ->
                            Expect.fail "マスの数が 5 個ではありません。"

                        Nothing ->
                            Expect.fail "検証対象の列が存在しません。"
            ]
        ]
src/Main.elm
module Main exposing (BingoCard(..), Cell(..), bingoCardGenerator)

import List.Extra as ListEx
import Random exposing (Generator)
import Random.Extra as RandomEx
import Random.List as RandomList


type Cell
    = Num Int
    | Free


type BingoCard
    = BingoCard (List (List Cell))


bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    let
        toColumnGenerator : List Int -> Generator (List Cell)
        toColumnGenerator ints =
            ints
                |> List.map Num
                |> RandomList.shuffle
                |> Random.map (List.take 5)
    in
    [ List.range 1 15 |> toColumnGenerator
    , List.range 16 30 |> toColumnGenerator
    , List.range 31 45
        |> toColumnGenerator
        |> Random.map (ListEx.setAt 2 Free)
    , [ Free, Free, Free, Free, Free ] |> Random.constant
    , [ Free, Free, Free, Free, Free ] |> Random.constant
    ]
        |> RandomEx.sequence
        |> Random.map BingoCard

G列には 46 以上 60 以下の値が設定される

特に変わったことはないので流し読み推奨。

テストコードを書きます:

verifyColumn "G列には 46 以上 60 以下の値が設定される" 3 46 60

これまでと同じようななんやかんやがあってプロダクトコードも実装します:

bingoCardGenerator
bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    let
        toColumnGenerator : List Int -> Generator (List Cell)
        toColumnGenerator ints =
            ints
                |> List.map Num
                |> RandomList.shuffle
                |> Random.map (List.take 5)
    in
    [ List.range 1 15 |> toColumnGenerator
    , List.range 16 30 |> toColumnGenerator
    , List.range 31 45
        |> toColumnGenerator
        |> Random.map (ListEx.setAt 2 Free)
    , List.range 46 60 |> toColumnGenerator  -- G列を生成
    , [ Free, Free, Free, Free, Free ] |> Random.constant
    ]
        |> RandomEx.sequence
        |> Random.map BingoCard

テストします:

$ elm-test

elm-test 0.19.0-rev6
--------------------

Running 5 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 149363890797378 elm-bingo-card-tdd\tests\Tests.elm


TEST RUN PASSED

Duration: 1141 ms
Passed:   5
Failed:   0

OK です。

途中経過7
tests/Tests.elm
module Tests exposing (suite)

import Expect exposing (Expectation)
import Fuzz exposing (Fuzzer)
import List.Extra as ListEx
import Main exposing (..)
import Random
import Test exposing (..)


bingoCardFuzzer : Fuzzer BingoCard
bingoCardFuzzer =
    let
        intToBingoCard : Int -> BingoCard
        intToBingoCard =
            Random.initialSeed >> Random.step bingoCardGenerator >> Tuple.first
    in
    Fuzz.map intToBingoCard Fuzz.int


verifyColumn : String -> Int -> Int -> Int -> Test
verifyColumn title index min max =
    fuzz bingoCardFuzzer title <|
        \(BingoCard columns) ->
            case ListEx.getAt index columns of
                Just [ Num n1, Num n2, Num n3, Num n4, Num n5 ] ->
                    [ n1, n2, n3, n4, n5 ]
                        |> List.all (\n -> min <= n && n <= max)
                        |> Expect.true "範囲外の値があります。"

                Just [ _, _, _, _, _ ] ->
                    Expect.fail "FREEマスが存在します。"

                Just _ ->
                    Expect.fail "マスの数が 5 個ではありません。"

                Nothing ->
                    Expect.fail "検証対象の列が存在しません。"


suite : Test
suite =
    describe "Main module"
        [ describe "bingoCardGenerator"
            [ fuzz bingoCardFuzzer "ビンゴカードは 5x5 のマスで構成される" <|
                \(BingoCard columns) ->
                    let
                        hasOnlyFiveElements : List a -> Bool
                        hasOnlyFiveElements aList =
                            List.length aList == 5
                    in
                    Expect.true "ビンゴカードが 5x5 のマスで構成されていません。" <|
                        hasOnlyFiveElements columns
                            && List.all hasOnlyFiveElements columns
            , verifyColumn "B列には 1 以上 15 以下の値が設定される" 0 1 15
            , verifyColumn "I列には 16 以上 30 以下の値が設定される" 1 16 30
            , fuzz bingoCardFuzzer "I列には 31 以上 45 以下の値が設定される" <|
                \(BingoCard columns) ->
                    case ListEx.getAt 2 columns of
                        Just [ Num n1, Num n2, Free, Num n4, Num n5 ] ->
                            [ n1, n2, n4, n5 ]
                                |> List.all (\n -> 31 <= n && n <= 45)
                                |> Expect.true "範囲外の値があります。"

                        Just [ _, _, Num n3, _, _ ] ->
                            Expect.fail "FREEマスの位置に数字マスが存在します。"

                        Just [ _, _, _, _, _ ] ->
                            Expect.fail "数字マスの位置にFREEマスが存在します。"

                        Just _ ->
                            Expect.fail "マスの数が 5 個ではありません。"

                        Nothing ->
                            Expect.fail "検証対象の列が存在しません。"
            , verifyColumn "G列には 46 以上 60 以下の値が設定される" 3 46 60
            ]
        ]
src/Main.elm
module Main exposing (BingoCard(..), Cell(..), bingoCardGenerator)

import List.Extra as ListEx
import Random exposing (Generator)
import Random.Extra as RandomEx
import Random.List as RandomList


type Cell
    = Num Int
    | Free


type BingoCard
    = BingoCard (List (List Cell))


bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    let
        toColumnGenerator : List Int -> Generator (List Cell)
        toColumnGenerator ints =
            ints
                |> List.map Num
                |> RandomList.shuffle
                |> Random.map (List.take 5)
    in
    [ List.range 1 15 |> toColumnGenerator
    , List.range 16 30 |> toColumnGenerator
    , List.range 31 45
        |> toColumnGenerator
        |> Random.map (ListEx.setAt 2 Free)
    , List.range 46 60 |> toColumnGenerator
    , [ Free, Free, Free, Free, Free ] |> Random.constant
    ]
        |> RandomEx.sequence
        |> Random.map BingoCard

O列には 61 以上 75 以下の値が設定される

特に変わったことはないので流し読み推奨。

テストコードを書きます:

verifyColumn "O列には 61 以上 75 以下の値が設定される" 4 61 75

これまでと同じようななんやかんやがあってプロダクトコードも実装します:

bingoCardGenerator
bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    let
        toColumnGenerator : List Int -> Generator (List Cell)
        toColumnGenerator ints =
            ints
                |> List.map Num
                |> RandomList.shuffle
                |> Random.map (List.take 5)
    in
    [ List.range 1 15 |> toColumnGenerator
    , List.range 16 30 |> toColumnGenerator
    , List.range 31 45
        |> toColumnGenerator
        |> Random.map (ListEx.setAt 2 Free)
    , List.range 46 60 |> toColumnGenerator
    , List.range 61 75 |> toColumnGenerator    ]
        |> RandomEx.sequence
        |> Random.map BingoCard

テストします:

$ elm-test

elm-test 0.19.0-rev6
--------------------

Running 6 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 343008546702977 elm-bingo-card-tdd\tests\Tests.elm


TEST RUN PASSED

Duration: 1436 ms
Passed:   6
Failed:   0

OK です。

途中経過8
tests/Tests.elm
module Tests exposing (suite)

import Expect exposing (Expectation)
import Fuzz exposing (Fuzzer)
import List.Extra as ListEx
import Main exposing (..)
import Random
import Test exposing (..)


bingoCardFuzzer : Fuzzer BingoCard
bingoCardFuzzer =
    let
        intToBingoCard : Int -> BingoCard
        intToBingoCard =
            Random.initialSeed >> Random.step bingoCardGenerator >> Tuple.first
    in
    Fuzz.map intToBingoCard Fuzz.int


verifyColumn : String -> Int -> Int -> Int -> Test
verifyColumn title index min max =
    fuzz bingoCardFuzzer title <|
        \(BingoCard columns) ->
            case ListEx.getAt index columns of
                Just [ Num n1, Num n2, Num n3, Num n4, Num n5 ] ->
                    [ n1, n2, n3, n4, n5 ]
                        |> List.all (\n -> min <= n && n <= max)
                        |> Expect.true "範囲外の値があります。"

                Just [ _, _, _, _, _ ] ->
                    Expect.fail "FREEマスが存在します。"

                Just _ ->
                    Expect.fail "マスの数が 5 個ではありません。"

                Nothing ->
                    Expect.fail "検証対象の列が存在しません。"


suite : Test
suite =
    describe "Main module"
        [ describe "bingoCardGenerator"
            [ fuzz bingoCardFuzzer "ビンゴカードは 5x5 のマスで構成される" <|
                \(BingoCard columns) ->
                    let
                        hasOnlyFiveElements : List a -> Bool
                        hasOnlyFiveElements aList =
                            List.length aList == 5
                    in
                    Expect.true "ビンゴカードが 5x5 のマスで構成されていません。" <|
                        hasOnlyFiveElements columns
                            && List.all hasOnlyFiveElements columns
            , verifyColumn "B列には 1 以上 15 以下の値が設定される" 0 1 15
            , verifyColumn "I列には 16 以上 30 以下の値が設定される" 1 16 30
            , fuzz bingoCardFuzzer "I列には 31 以上 45 以下の値が設定される" <|
                \(BingoCard columns) ->
                    case ListEx.getAt 2 columns of
                        Just [ Num n1, Num n2, Free, Num n4, Num n5 ] ->
                            [ n1, n2, n4, n5 ]
                                |> List.all (\n -> 31 <= n && n <= 45)
                                |> Expect.true "範囲外の値があります。"

                        Just [ _, _, Num n3, _, _ ] ->
                            Expect.fail "FREEマスの位置に数字マスが存在します。"

                        Just [ _, _, _, _, _ ] ->
                            Expect.fail "数字マスの位置にFREEマスが存在します。"

                        Just _ ->
                            Expect.fail "マスの数が 5 個ではありません。"

                        Nothing ->
                            Expect.fail "検証対象の列が存在しません。"
            , verifyColumn "G列には 46 以上 60 以下の値が設定される" 3 46 60
            , verifyColumn "O列には 61 以上 75 以下の値が設定される" 4 61 75
            ]
        ]
src/Main.elm
module Main exposing (BingoCard(..), Cell(..), bingoCardGenerator)

import List.Extra as ListEx
import Random exposing (Generator)
import Random.Extra as RandomEx
import Random.List as RandomList


type Cell
    = Num Int
    | Free


type BingoCard
    = BingoCard (List (List Cell))


bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    let
        toColumnGenerator : List Int -> Generator (List Cell)
        toColumnGenerator ints =
            ints
                |> List.map Num
                |> RandomList.shuffle
                |> Random.map (List.take 5)
    in
    [ List.range 1 15 |> toColumnGenerator
    , List.range 16 30 |> toColumnGenerator
    , List.range 31 45
        |> toColumnGenerator
        |> Random.map (ListEx.setAt 2 Free)
    , List.range 46 60 |> toColumnGenerator
    , List.range 61 75 |> toColumnGenerator
    ]
        |> RandomEx.sequence
        |> Random.map BingoCard

同じ値のマスは 1個のみ存在する

5x5 のマスの値をパターンマッチで取り出し、それらの中で重複がないかを検証します。
テストコードはこうなります:

import List.Extra as ListEx

fuzz bingoCardFuzzer "同じ値のマスは 1個のみ存在する" <|
    \(BingoCard columns) ->
        case columns of
            -- 無慈悲な elm-format によりパターンマッチのリストは 1行にまとめられてしまう。
            -- 列ごとに改行したいが諦める。無念。
            [ [ Num b1, Num b2, Num b3, Num b4, Num b5 ], [ Num i1, Num i2, Num i3, Num i4, Num i5 ], [ Num n1, Num n2, Free, Num n4, Num n5 ], [ Num g1, Num g2, Num g3, Num g4, Num g5 ], [ Num o1, Num o2, Num o3, Num o4, Num o5 ] ] ->
                -- 列ごとに改行するだけだと elm-format により 1行に 1要素の形に展開され、
                -- コードが縦長になってしまう。
                -- これを防ぐため、列ごとにリストでまとめ (++) で結合するようにした。
                -- 無駄な処理だが読みやすさを重視する。
                [ b1, b2, b3, b4, b5 ]
                    ++ [ i1, i2, i3, i4, i5 ]
                    ++ [ n1, n2, n4, n5 ]
                    ++ [ g1, g2, g3, g4, g5 ]
                    ++ [ o1, o2, o3, o4, o5 ]
                    |> ListEx.allDifferent
                    |> Expect.true "同じ値が複数存在します。"

            _ ->
                Expect.fail <|
                    "ビンゴカードのマスの配置が誤っています。\n"
                        ++ "他のテストが失敗しているはずなので、詳細はそちらを確認してください。"

List.Extra.allDifferent はリスト内に同じ要素が存在しないかを判定する関数です (同じ要素が存在しない場合に True) :

List.Extra.allDifferent
allDifferent : List comparable -> Bool

なお、パターンマッチ部分は次のように改行しておきたかったのですが、elm-format がそれを許してくれませんでした。

パターンマッチ部分の理想の形
case columns of
    [ [ Num b1, Num b2, Num b3, Num b4, Num b5 ]
    , [ Num i1, Num i2, Num i3, Num i4, Num i5 ]
    , [ Num n1, Num n2, Free, Num n4, Num n5 ]
    , [ Num g1, Num g2, Num g3, Num g4, Num g5 ]
    , [ Num o1, Num o2, Num o3, Num o4, Num o5 ]
    ] ->
        :

他の場合は問題ない (改行された状態を保つ) のです、パターンマッチの場合は駄目みたいです。
-> の左側は複数行を許可してないのかもしれません。わからん、全然わからん。

コードの整形についてはひとまず置いておいて、このテストコードもテストします。
たとえば中央に FREEマスがないビンゴカードを生成するよう bingoCardGenerator を変更します:

bingoCardGenerator
bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    let
        toColumnGenerator : List Int -> Generator (List Cell)
        toColumnGenerator ints =
            ints
                |> List.map Num
                |> RandomList.shuffle
                |> Random.map (List.take 5)
    in
    [ List.range 1 15 |> toColumnGenerator
    , List.range 16 30 |> toColumnGenerator
    , List.range 31 45
        |> toColumnGenerator
-       |> Random.map (ListEx.setAt 2 Free) 
    , List.range 46 60 |> toColumnGenerator
    , List.range 61 75 |> toColumnGenerator
    ]
        |> RandomEx.sequence
        |> Random.map BingoCard

この状態で elm-test を実行すると次のような結果になります:

elm-test実行結果
> N列には 31 以上 45 以下の値が設定される

Given BingoCard [[Num 11,Num 8,Num 7,Num 4,Num 2],[Num 26,Num 16,Num 30,Num 25,Num 24],[Num 32,Num 45,Num 43,Num 42,Num 41],[Num 46,Num 60,Num 58,Num 57,Num 51],[Num 74,Num 73,Num 66,Num 67,Num 72]]

    FREEマスの位置に数字マスが存在します。

:
> 同じ値のマスは 1個のみ存在する

Given BingoCard [[Num 11,Num 8,Num 7,Num 4,Num 2],[Num 26,Num 16,Num 30,Num 25,Num 24],[Num 32,Num 45,Num 43,Num 42,Num 41],[Num 46,Num 60,Num 58,Num 57,Num 51],[Num 74,Num 73,Num 66,Num 67,Num 72]]

    ビンゴカードのマスの配置が誤っています。
    他のテストが失敗しているはずなので、詳細はそちらを確認してください。

ある列のマスが少ない場合はこんな感じです:

bingoCardGenerator
bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    let
        toColumnGenerator : List Int -> Generator (List Cell)
        toColumnGenerator ints =
            ints
                |> List.map Num
                |> RandomList.shuffle
                |> Random.map (List.take 5)
    in
    [ List.range 1 15 |> toColumnGenerator
-   , List.range 16 30 |> toColumnGenerator
+   , [ Num 16, Num 17, Num 29, Num 30 ] |> Random.constant
    , List.range 31 45
        |> toColumnGenerator
        |> Random.map (ListEx.setAt 2 Free)
    , List.range 46 60 |> toColumnGenerator
    , List.range 61 75 |> toColumnGenerator
    ]
        |> RandomEx.sequence
        |> Random.map BingoCard
elm-test実行結果
> ビンゴカードは 5x5 のマスで構成される

Given BingoCard [[Num 11,Num 8,Num 7,Num 4,Num 2],[Num 16,Num 17,Num 29,Num 30],[Num 41,Num 31,Free,Num 40,Num 39],[Num 47,Num 60,Num 58,Num 57,Num 56],[Num 61,Num 75,Num 73,Num 72,Num
66]]

    ビンゴカードが 5x5 のマスで構成されていません。

:
> I列には 16 以上 30 以下の値が設定される

Given BingoCard [[Num 11,Num 8,Num 7,Num 4,Num 2],[Num 16,Num 17,Num 29,Num 30],[Num 41,Num 31,Free,Num 40,Num 39],[Num 47,Num 60,Num 58,Num 57,Num 56],[Num 61,Num 75,Num 73,Num 72,Num
66]]

    マスの数が 5 個ではありません。

:
> 同じ値のマスは 1個のみ存在する

Given BingoCard [[Num 11,Num 8,Num 7,Num 4,Num 2],[Num 16,Num 17,Num 29,Num 30],[Num 41,Num 31,Free,Num 40,Num 39],[Num 47,Num 60,Num 58,Num 57,Num 56],[Num 61,Num 75,Num 73,Num 72,Num
66]]

    ビンゴカードのマスの配置が誤っています。
    他のテストが失敗しているはずなので、詳細はそちらを確認してください。

という感じでテストコードをなんやかんやとテストしていきます。
一通りテストしたらプロダクトコードを元の状態に戻してテストします:

$ elm-test

elm-test 0.19.0-rev6
--------------------

Running 7 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 230632092059212 elm-bingo-card-tdd\tests\Tests.elm


TEST RUN PASSED

Duration: 1448 ms
Passed:   7
Failed:   0

OK です。

途中経過9
tests/Tests.elm
module Tests exposing (suite)

import Expect exposing (Expectation)
import Fuzz exposing (Fuzzer)
import List.Extra as ListEx
import Main exposing (..)
import Random
import Test exposing (..)


bingoCardFuzzer : Fuzzer BingoCard
bingoCardFuzzer =
    let
        intToBingoCard : Int -> BingoCard
        intToBingoCard =
            Random.initialSeed >> Random.step bingoCardGenerator >> Tuple.first
    in
    Fuzz.map intToBingoCard Fuzz.int


verifyColumn : String -> Int -> Int -> Int -> Test
verifyColumn title index min max =
    fuzz bingoCardFuzzer title <|
        \(BingoCard columns) ->
            case ListEx.getAt index columns of
                Just [ Num n1, Num n2, Num n3, Num n4, Num n5 ] ->
                    [ n1, n2, n3, n4, n5 ]
                        |> List.all (\n -> min <= n && n <= max)
                        |> Expect.true "範囲外の値があります。"

                Just [ _, _, _, _, _ ] ->
                    Expect.fail "FREEマスが存在します。"

                Just _ ->
                    Expect.fail "マスの数が 5 個ではありません。"

                Nothing ->
                    Expect.fail "検証対象の列が存在しません。"


suite : Test
suite =
    describe "Main module"
        [ describe "bingoCardGenerator"
            [ fuzz bingoCardFuzzer "ビンゴカードは 5x5 のマスで構成される" <|
                \(BingoCard columns) ->
                    let
                        hasOnlyFiveElements : List a -> Bool
                        hasOnlyFiveElements aList =
                            List.length aList == 5
                    in
                    Expect.true "ビンゴカードが 5x5 のマスで構成されていません。" <|
                        hasOnlyFiveElements columns
                            && List.all hasOnlyFiveElements columns
            , verifyColumn "B列には 1 以上 15 以下の値が設定される" 0 1 15
            , verifyColumn "I列には 16 以上 30 以下の値が設定される" 1 16 30
            , fuzz bingoCardFuzzer "N列には 31 以上 45 以下の値が設定される" <|
                \(BingoCard columns) ->
                    case ListEx.getAt 2 columns of
                        Just [ Num n1, Num n2, Free, Num n4, Num n5 ] ->
                            [ n1, n2, n4, n5 ]
                                |> List.all (\n -> 31 <= n && n <= 45)
                                |> Expect.true "範囲外の値があります。"

                        Just [ _, _, Num n3, _, _ ] ->
                            Expect.fail "FREEマスの位置に数字マスが存在します。"

                        Just [ _, _, _, _, _ ] ->
                            Expect.fail "数字マスの位置にFREEマスが存在します。"

                        Just _ ->
                            Expect.fail "マスの数が 5 個ではありません。"

                        Nothing ->
                            Expect.fail "検証対象の列が存在しません。"
            , verifyColumn "G列には 46 以上 60 以下の値が設定される" 3 46 60
            , verifyColumn "O列には 61 以上 75 以下の値が設定される" 4 61 75
            , fuzz bingoCardFuzzer "同じ値のマスは 1個のみ存在する" <|
                \(BingoCard columns) ->
                case columns of
                    -- 無慈悲な elm-format によりパターンマッチのリストは 1行にまとめられてしまう。
                    -- 列ごとに改行したいが諦める。無念。
                    [ [ Num b1, Num b2, Num b3, Num b4, Num b5 ], [ Num i1, Num i2, Num i3, Num i4, Num i5 ], [ Num n1, Num n2, Free, Num n4, Num n5 ], [ Num g1, Num g2, Num g3, Num g4, Num g5 ], [ Num o1, Num o2, Num o3, Num o4, Num o5 ] ] ->
                        -- 列ごとに改行するだけだと elm-format により 1行に 1要素の形に展開され、
                        -- コードが縦長になってしまう。
                        -- これを防ぐため、列ごとにリストでまとめ (++) で結合するようにした。
                        -- 無駄な処理だが読みやすさを重視する。
                        [ b1, b2, b3, b4, b5 ]
                            ++ [ i1, i2, i3, i4, i5 ]
                            ++ [ n1, n2, n4, n5 ]
                            ++ [ g1, g2, g3, g4, g5 ]
                            ++ [ o1, o2, o3, o4, o5 ]
                            |> ListEx.allDifferent
                            |> Expect.true "同じ値が複数存在します。"

                    _ ->
                        Expect.fail <|
                            "ビンゴカードのマスの配置が誤っています。\n"
                                ++ "他のテストが失敗しているはずなので、詳細はそちらを確認してください。"
            ]
        ]

Main.elm は変更なしです。

src/Main.elm
module Main exposing (BingoCard(..), Cell(..), bingoCardGenerator)

import List.Extra as ListEx
import Random exposing (Generator)
import Random.Extra as RandomEx
import Random.List as RandomList


type Cell
    = Num Int
    | Free


type BingoCard
    = BingoCard (List (List Cell))


bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    let
        toColumnGenerator : List Int -> Generator (List Cell)
        toColumnGenerator ints =
            ints
                |> List.map Num
                |> RandomList.shuffle
                |> Random.map (List.take 5)
    in
    [ List.range 1 15 |> toColumnGenerator
    , List.range 16 30 |> toColumnGenerator
    , List.range 31 45
        |> toColumnGenerator
        |> Random.map (ListEx.setAt 2 Free)
    , List.range 46 60 |> toColumnGenerator
    , List.range 61 75 |> toColumnGenerator
    ]
        |> RandomEx.sequence
        |> Random.map BingoCard

中央のマス (N列の 3段目) 常に FREEマス、それ以外は数字マス

1つ前のテストで 5x5 のマスの値をパターンマッチで取り出して検証 というテストコードにしたので、それをコピペすればほぼ完了です。
値の判定はしないのでそこだけ変更します:

fuzz bingoCardFuzzer "中央のマス (N列の 3段目) 常に FREEマス、それ以外は数字マス" <|
    \(BingoCard columns) ->
        case columns of
            [ [ Num b1, Num b2, Num b3, Num b4, Num b5 ], [ Num i1, Num i2, Num i3, Num i4, Num i5 ], [ Num n1, Num n2, Free, Num n4, Num n5 ], [ Num g1, Num g2, Num g3, Num g4, Num g5 ], [ Num o1, Num o2, Num o3, Num o4, Num o5 ] ] ->
                Expect.pass

            _ ->
                Expect.fail <|
                    "ビンゴカードのマスの配置が誤っています。\n"
                        ++ "他のテストが失敗しているはずなので、詳細はそちらを確認してください。"

先のテストと同じようになんやかんやの後にプロダクトコードをテストします:

$ elm-test

elm-test 0.19.0-rev6
--------------------

Running 8 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 152152025322505 elm-bingo-card-tdd\tests\Tests.elm


TEST RUN PASSED

Duration: 1304 ms
Passed:   8
Failed:   0

OK です。

同じ値のマスは 1個のみ存在する のテストと検証内容はほぼ変わらないため「このテストコードは不要では?」と思いましたが、テストコードは 仕様を読み取れること も重要なのでこのコードも残しておくことにしました。

そもそも同じ値のマスの個数を確認するのにパターンマッチは過剰だったかもしれません…。
が、パターンマッチしない場合は値を取り出すのがちょっと面倒になります。
なので今回のテストコードの構成は悪くないものだった、と考えておくことにします。

最終的なコード
tests/Tests.elm
module Tests exposing (suite)

import Expect exposing (Expectation)
import Fuzz exposing (Fuzzer)
import List.Extra as ListEx
import Main exposing (..)
import Random
import Test exposing (..)


bingoCardFuzzer : Fuzzer BingoCard
bingoCardFuzzer =
    let
        intToBingoCard : Int -> BingoCard
        intToBingoCard =
            Random.initialSeed >> Random.step bingoCardGenerator >> Tuple.first
    in
    Fuzz.map intToBingoCard Fuzz.int


verifyColumn : String -> Int -> Int -> Int -> Test
verifyColumn title index min max =
    fuzz bingoCardFuzzer title <|
        \(BingoCard columns) ->
            case ListEx.getAt index columns of
                Just [ Num n1, Num n2, Num n3, Num n4, Num n5 ] ->
                    [ n1, n2, n3, n4, n5 ]
                        |> List.all (\n -> min <= n && n <= max)
                        |> Expect.true "範囲外の値があります。"

                Just [ _, _, _, _, _ ] ->
                    Expect.fail "FREEマスが存在します。"

                Just _ ->
                    Expect.fail "マスの数が 5 個ではありません。"

                Nothing ->
                    Expect.fail "検証対象の列が存在しません。"


suite : Test
suite =
    describe "Main module"
        [ describe "bingoCardGenerator"
            [ fuzz bingoCardFuzzer "ビンゴカードは 5x5 のマスで構成される" <|
                \(BingoCard columns) ->
                    let
                        hasOnlyFiveElements : List a -> Bool
                        hasOnlyFiveElements aList =
                            List.length aList == 5
                    in
                    Expect.true "ビンゴカードが 5x5 のマスで構成されていません。" <|
                        hasOnlyFiveElements columns
                            && List.all hasOnlyFiveElements columns
            , verifyColumn "B列には 1 以上 15 以下の値が設定される" 0 1 15
            , verifyColumn "I列には 16 以上 30 以下の値が設定される" 1 16 30
            , fuzz bingoCardFuzzer "N列には 31 以上 45 以下の値が設定される" <|
                \(BingoCard columns) ->
                    case ListEx.getAt 2 columns of
                        Just [ Num n1, Num n2, Free, Num n4, Num n5 ] ->
                            [ n1, n2, n4, n5 ]
                                |> List.all (\n -> 31 <= n && n <= 45)
                                |> Expect.true "範囲外の値があります。"

                        Just [ _, _, Num n3, _, _ ] ->
                            Expect.fail "FREEマスの位置に数字マスが存在します。"

                        Just [ _, _, _, _, _ ] ->
                            Expect.fail "数字マスの位置にFREEマスが存在します。"

                        Just _ ->
                            Expect.fail "マスの数が 5 個ではありません。"

                        Nothing ->
                            Expect.fail "検証対象の列が存在しません。"
            , verifyColumn "G列には 46 以上 60 以下の値が設定される" 3 46 60
            , verifyColumn "O列には 61 以上 75 以下の値が設定される" 4 61 75
            , fuzz bingoCardFuzzer "同じ値のマスは 1個のみ存在する" <|
                \(BingoCard columns) ->
                    case columns of
                        -- 無慈悲な elm-format によりパターンマッチのリストは 1行にまとめられてしまう。
                        -- 列ごとに改行したいが諦める。無念。
                        [ [ Num b1, Num b2, Num b3, Num b4, Num b5 ], [ Num i1, Num i2, Num i3, Num i4, Num i5 ], [ Num n1, Num n2, Free, Num n4, Num n5 ], [ Num g1, Num g2, Num g3, Num g4, Num g5 ], [ Num o1, Num o2, Num o3, Num o4, Num o5 ] ] ->
                            -- 列ごとに改行するだけだと elm-format により 1行に 1要素の形に展開され、
                            -- コードが縦長になってしまう。
                            -- これを防ぐため、列ごとにリストでまとめ (++) で結合するようにした。
                            -- 無駄な処理だが読みやすさを重視する。
                            [ b1, b2, b3, b4, b5 ]
                                ++ [ i1, i2, i3, i4, i5 ]
                                ++ [ n1, n2, n4, n5 ]
                                ++ [ g1, g2, g3, g4, g5 ]
                                ++ [ o1, o2, o3, o4, o5 ]
                                |> ListEx.allDifferent
                                |> Expect.true "同じ値が複数存在します。"

                        _ ->
                            Expect.fail <|
                                "ビンゴカードのマスの配置が誤っています。\n"
                                    ++ "他のテストが失敗しているはずなので、詳細はそちらを確認してください。"
            , fuzz bingoCardFuzzer "中央のマス (N列の 3段目) 常に FREEマス、それ以外は数字マス" <|
                \(BingoCard columns) ->
                    case columns of
                        [ [ Num b1, Num b2, Num b3, Num b4, Num b5 ], [ Num i1, Num i2, Num i3, Num i4, Num i5 ], [ Num n1, Num n2, Free, Num n4, Num n5 ], [ Num g1, Num g2, Num g3, Num g4, Num g5 ], [ Num o1, Num o2, Num o3, Num o4, Num o5 ] ] ->
                            Expect.pass

                        _ ->
                            Expect.fail <|
                                "ビンゴカードのマスの配置が誤っています。\n"
                                    ++ "他のテストが失敗しているはずなので、詳細はそちらを確認してください。"
            ]
        ]
src/Main.elm
module Main exposing (BingoCard(..), Cell(..), bingoCardGenerator)

import List.Extra as ListEx
import Random exposing (Generator)
import Random.Extra as RandomEx
import Random.List as RandomList


type Cell
    = Num Int
    | Free


type BingoCard
    = BingoCard (List (List Cell))


bingoCardGenerator : Generator BingoCard
bingoCardGenerator =
    let
        toColumnGenerator : List Int -> Generator (List Cell)
        toColumnGenerator ints =
            ints
                |> List.map Num
                |> RandomList.shuffle
                |> Random.map (List.take 5)
    in
    [ List.range 1 15 |> toColumnGenerator
    , List.range 16 30 |> toColumnGenerator
    , List.range 31 45
        |> toColumnGenerator
        |> Random.map (ListEx.setAt 2 Free)
    , List.range 46 60 |> toColumnGenerator
    , List.range 61 75 |> toColumnGenerator
    ]
        |> RandomEx.sequence
        |> Random.map BingoCard

完成

テストが全て OK だったので bingoCardGenerator は仕様通りの実装ができていると思っていいでしょう。
これで完成です。

あとはビンゴカードを表示する部分やビンゴカード生成の起点となるイベントを管理する部分が必要ですね。
その内やります。やるんじゃないかな。やるといいな。

fuzz の補足

fuzz で 100パターンのデータが用意されるってのはホンマなんかワレ?」という疑問があるかもしれません。
テストを全部やっても結果が Passed: 8 とかですし、実際には 1パターンかもしれませんよね。
というわけで print デバッグで確認しましょう。
Elm で print デバッグする方法は @miyamo_madoka さんによる Elm初心者でもできるprintデバッグ が参考になります。

今回作成したテストの内、2件に対して次のように行を追加します:

printデバッグ用のコードを追加
            [ fuzz bingoCardFuzzer "ビンゴカードは 5x5 のマスで構成される" <|
                \(BingoCard columns) ->
                    let
+                       _ = Debug.log "test1-columns" columns
                        hasOnlyFiveElements : List a -> Bool
                        hasOnlyFiveElements aList =
                            List.length aList == 5
                    in
                    Expect.true "ビンゴカードが 5x5 のマスで構成されていません。" <|
                        hasOnlyFiveElements columns
                            && List.all hasOnlyFiveElements columns
            , verifyColumn "B列には 1 以上 15 以下の値が設定される" 0 1 15
            , verifyColumn "I列には 16 以上 30 以下の値が設定される" 1 16 30
            , fuzz bingoCardFuzzer "N列には 31 以上 45 以下の値が設定される" <|
                \(BingoCard columns) ->
+                   let
+                       _ = Debug.log "test2-columns" columns
+                   in
                    case ListEx.getAt 2 columns of
                        :

この状態でテストを実行します:

elm-test実行結果
$ elm-test

elm-test 0.19.0-rev6
--------------------

Running 8 tests. To reproduce these results, run: elm-test --fuzz 100 --seed 256308025512112 elm-bingo-card-tdd\tests\Tests.elm

test1-columns: [[Num 13,Num 12,Num 11,Num 10,Num 2],[Num 22,Num 21,Num 28,Num 27,Num 25],[Num 40,Num 38,Free,Num 41,Num 37],[Num 47,Num 50,Num 55,Num 51,Num 46],[Num 75,Num 74,Num 73,Num 70,Num 71]]
test2-columns: [[Num 5,Num 4,Num 15,Num 11,Num 6],[Num 29,Num 30,Num 28,Num 27,Num 26],[Num 37,Num 36,Free,Num 41,Num 31],[Num 46,Num 59,Num 58,Num 55,Num 50],[Num 71,Num 62,Num 74,Num 65,Num 61]]
test1-columns: [[Num 8,Num 13,Num 4,Num 7,Num 2],[Num 24,Num 22,Num 26,Num 16,Num 21],[Num 40,Num 42,Free,Num 32,Num 35],[Num 53,Num 52,Num 51,Num 50,Num 49],[Num 62,Num 61,Num 75,Num 74,Num 69]]
test1-columns: [[Num 8,Num 4,Num 10,Num 3,Num 7],[Num 18,Num 17,Num 30,Num 29,Num 21],[Num 45,Num 44,Free,Num 42,Num 41],[Num 60,Num 59,Num 57,Num 46,Num 56],[Num 62,Num 75,Num 73,Num 71,Num 74]]
test2-columns: [[Num 11,Num 8,Num 7,Num 4,Num 2],[Num 26,Num 16,Num 30,Num 25,Num 24],[Num 32,Num 45,Free,Num 42,Num 41],[Num 46,Num 60,Num 58,Num 57,Num 51],[Num 74,Num 73,Num 66,Num 67,Num 72]]
test2-columns: [[Num 7,Num 5,Num 3,Num 2,Num 10],[Num 22,Num 21,Num 29,Num 30,Num 20],[Num 31,Num 39,Free,Num 42,Num 35],[Num 59,Num 57,Num 55,Num 54,Num 53],[Num 63,Num 62,Num 61,Num 68,Num 67]]
test1-columns: [[Num 6,Num 4,Num 2,Num 9,Num 5],[Num 21,Num 20,Num 25,Num 19,Num 18],[Num 36,Num 34,Free,Num 43,Num 41],[Num 55,Num 53,Num 54,Num 52,Num 56],[Num 62,Num 61,Num 75,Num 72,Num 71]]
test1-columns: [[Num 6,Num 5,Num 4,Num 3,Num 2],[Num 27,Num 24,Num 23,Num 19,Num 22],[Num 39,Num 45,Free,Num 44,Num 43],[Num 48,Num 47,Num 49,Num 57,Num 56],[Num 66,Num 65,Num 62,Num 61,Num 74]]
test2-columns: [[Num 6,Num 5,Num 4,Num 13,Num 15],[Num 24,Num 16,Num 21,Num 30,Num 22],[Num 40,Num 45,Free,Num 38,Num 32],[Num 48,Num 55,Num 49,Num 50,Num 47],[Num 68,Num 67,Num 64,Num 75,Num 71]]
:

TEST RUN PASSED

Duration: 2383 ms
Passed:   8
Failed:   0

長いので割愛しましたが、test1-columns の行と test2-columns の行がそれぞれ 100行ずつ出力されました。
実際にテスト 1件につき 100パターンのビンゴカードが作成されているようです。

また、出力をよく見ると test1-columns の行と test2-columns の行が不規則に出現しているのが分かります。
elm-test は各テストを 1件ずつ実行するのではなく、複数のテストを同時に実行しているようです。
Elm の世界には副作用が存在しないので 1件ずつ実行する必要性がないということでしょう。なるほどなー。

おまけ

elm-explorations/test パッケージ の文を引用して終わります。

Not even your test modules can import unexposed functions, so test them only as the exposed interface uses them. Don't expose a function just to test it. Every exposed function should have tests. (If you practice TDD, this happens automatically!)

テストするためだけに関数を公開しないでください。

src/Main.elm
module Main exposing (BingoCard(..), Cell(..), bingoCardGenerator)

ああ…。

Avoid importing your Main module. Most of your code belongs in other modules, so import those instead.

Main モジュールをインポートしないでください。

tests/Tests.elm
import Main exposing (..)

あああ…。

許して…許して…。

おしまり

続き


  1. elm-analyze の説明では "Using a record is obsolete if you only plan to store a single field in it." とあるので、将来的にフィールドが複数になるのであれば問題視するつもりはない模様。Elmガイドの内容とも矛盾しない。でもちょっと引っかかる…。 

  2. 基礎からわかるElm の著者。 

  3. 100 はデフォルト値。この値は elm-test のオプション --fuzz で指定可能。 

  4. なぜか同じ機能の combine という関数も Random.Extra モジュールに存在する 。追加されたのは combine の方が先。 

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3