Elmでもテストを書こう

Elmでテストを書きましょう。  
この文章は[Elm2 Advent Calendar 2017](https://qiita.com/advent-calendar/2017/elm2)の22日目の投稿です。
昨日は @cyclone_t さんの [YouTube IFrame Player APIをElmから利用する](https://qiita.com/cyclone_t/items/04ebec794b200d63cf51) でした。

### 対象者

Elmのテストに興味がある人が対象になります。  
ある程度、他言語でユニットテストを書いたことがある方が望ましいです。

### わかること

- Elmのテスティングライブラリはどんなものか
- ElmのテストをCLI上で行うにはどうするのか
- 文字列の一致をテストするElmのテストコードの書き方

それでは始めましょう

### Elmのテスティングライブラリはどんなものか

Elmには他の言語同様にテストをサポートするライブラリが用意されています。

[elm-community/elm-test](https://github.com/elm-community/elm-test)

```elm
suite : Test
suite =
    describe "The String module"
        [ describe "String.reverse" -- Nest as many descriptions as you like.
            [ test "has no effect on a palindrome" <|
                \_ ->
                    let
                        palindrome =
                            "hannah"
                    in
                        Expect.equal palindrome (String.reverse palindrome)

            -- Expect.equal is designed to be used in pipeline style, like this.
            , test "reverses a known string" <|
                \_ ->
                    "ABCDEFG"
                        |> String.reverse
                        |> Expect.equal "GFEDCBA"

            -- fuzz runs the test 100 times with randomly-generated inputs!
            , fuzz string "restores the original string if you run it again" <|
                \randomlyGeneratedString ->
                    randomlyGeneratedString
                        |> String.reverse
                        |> String.reverse
                        |> Expect.equal randomlyGeneratedString
            ]
        ]
```

elm-testがElmにおけるテスティングフレームワークです。テストコードに必要なモジュール一式が提供されています。特徴的なのはファジングを気軽に実行できることです。

>ファジングとは、ソフトウェアの不具合（とくに脆弱性を意図することが多い）を発見するためのテスト手法の一つである。ファズ（英：fuzz）（予測不可能な入力データ）を与えることで意図的に例外を発生させ、その例外の挙動を確認するという方法を用いる。ファズテストと呼ばれることもある。
>[ファジング - Wikipedia](https://ja.wikipedia.org/wiki/%E3%83%95%E3%82%A1%E3%82%B8%E3%83%B3%E3%82%B0)

Fuzzモジュールはファジングを強力にサポートします。seedからランダムな入力データを自動で生成してテスト対象コードの入力とすることが可能です。

[Fuzz - elm-test 4.2.0](http://package.elm-lang.org/packages/elm-community/elm-test/latest/Fuzz)

String型の生成データを例に見ても `""`, `"\n"` のような罠となりそうな文字列も生成してくれます。入力を自前で用意しないといけない手間が省ける点でも利用をおすすめできます。

詳しくは Elm2 Advent Calendar で @jinjor さんが記事をかかれています。要チェックですよ!

[[Elm] Fuzzer でテストデータを量産しよう](https://qiita.com/jinjor/items/429bf132119775a6f859)

### ElmのテストをCLI上で行うにはどうするのか

Elmのテストコードはブラウザ上で結果を確認する方法とコンソール上で確認する方法が存在します。CIの都合上、一般的にはコンソール上で確認するシーンが多いと思いますのでコンソール上で実行するためのテストランナーをご紹介します。

[rtfeldman/node-test-runner](https://github.com/rtfeldman/node-test-runner)

`node-test-runnner` はテストの実行をサポートするだけではなく、初期テスト環境を自動構築してくれます。

```
$ elm-test init
Created ./src
Created ./elm-package.json
Created ./tests/elm-package.json
Created ./tests/Example.elm
Created ./.gitignore
Starting downloads...

  ● eeue56/elm-lazy-list 1.0.0
  ● eeue56/elm-lazy 1.0.0
  ● eeue56/elm-shrink 1.0.0
  ● elm-community/elm-test 4.2.0
  ● elm-lang/html 2.0.0
  ● elm-lang/virtual-dom 2.0.4
  ● mgold/elm-random-pcg 5.0.2
  ● elm-lang/core 5.1.1

Packages configured successfully!
```

テストコードが書けたらテストを実行しましょう

```
$ elm-test
Success! Compiled 0 modules.                                        
Successfully generated /dev/null
Success! Compiled 1 module.                                         
Successfully generated ./elm-stuff/generated-code/elm-community/elm-test/elmTestOutput.js

elm-test 0.18.10
----------------

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


TEST RUN INCOMPLETE because there is 1 TODO remaining

Duration: 242 ms
Passed:   0
Failed:   0
Todo:     1
↓ Example
◦ TODO: Implement our first test. See http://package.elm-lang.org/packages/elm-community/elm-test/latest for how to do this!
```

テスト結果に注目してください。Todoがあるのがわかると思います。これはテストコード内にtodoを設定しておくことができるAPIが提供されているためです。
それでは実際にテストコードを書いてみましょう

### Elmのテストコードの書き方

`elm-test init` で作成された `Example.elm` を少し変更してみます。

```elm
module Example exposing (..)

import Expect exposing (Expectation)
import Fuzz exposing (Fuzzer, int, list, string)
import Test exposing (..)


suite : Test
suite =
    describe "Example Test Suite"
        [ test "should equal" <|
            \() ->
                description
                    |> Expect.equal "Elm is functional language."
        ]

description : String
description =
    "Elm is functional language."
```

これは文字列 `Elm is functional language.` の一致をテストしています。  
`describe` は `Test` モジュールが提供しています。これを利用するとテストの名前をつけることができます。ここで定義しておくとテスト失敗時にどのテストケースが失敗したのかがわかりやすくなるので設定をおすすめします。第二引数がリストになっているのは複数のテストケースを渡すことができるからです。テストケースは `test` で実行しています。第一引数でテストケースに名前をつけています。第二引数でテストの詳細を実装します。実際には関数の実行結果をテストすることになります。


```
elm-test 0.18.10
----------------

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

↓ Example
↓ Example Test Suite
✗ should equal

    "Elm is functional language."
    ╷
    │ Expect.equal
    ╵
    "Elm is functionall language."



TEST RUN FAILED

Duration: 261 ms
Passed:   0
Failed:   1
```

テストをわざと失敗させてみました。するとこのように失敗したテストの詳細がターミナル上に出力されます。  

等価性を判定する `Expect.equal` の他にも様々なAPIが提供されています。

[Expect - elm-test 4.2.0](http://package.elm-lang.org/packages/elm-community/elm-test/4.2.0/Expect)

より実践的なテストコードの書き方は `node-test-runner` のexampleコードが参考になります。

[node-test-runner/TestsFailing.elm at master](https://github.com/rtfeldman/node-test-runner/blob/master/example/tests/TestsFailing.elm)

このテストコードでは先に説明したFuzzモジュールを使ったテストも体系的に学ぶことができるので、 `git clone` コマンドを実行して是非手元で試してみてください。

### まとめ

- elm-testを使おう
- node-test-runnerを使ってテストを実行してみよう
- Expectモジュールが提供するAPIを使ってテストコードを書こう

明日は @zaneli@github さんの [Elm + native moduleでドラッグ&ドロップしたファイルの情報を取得する](https://www.zaneli.com/blog/20171223) です!