Stackで単体テストを書いて実行する - Haskell入門より

このエントリは、 [Haskell入門の執筆](http://hiratara.hatenadiary.jp/entry/2017/09/17/104015) の際に、 ページ数の都合で載せられなかった記事に加筆修正をしたものです。1.4節に載せる予定だった内容です。

このエントリの内容を試すには、 [Stackをインストール](https://docs.haskellstack.org/en/stable/README/#how-to-install) し、 `/tmp` ディレクトリなどで以下のコマンドを実行してプロジェクトを作成してください。

```console
$ stack new myproject
$ cd myproject/
```

### 初めての開発: 単体テストを書く

開発フローを体験するため、未実装だった単体テストを作ってみましょう。開発をするときにはREPLを使うのも便利ですが、ここでは `--file-watch` オプションを使います。`--file-watch` オプションを `stack build` へ指定すると、プログラムに変更があるかを監視し、変更があった場合は自動的にビルドをしてくれます。さらに、 `build` に`--test` オプションを渡すと `stack test` と同等の機能になります。このようにして`stack build` を常時走らせておくと、プログラムにエラーがあった時にすぐに見つけることができます。

```console
$ stack build --test --file-watch
.. 略 ..

myproject-0.1.0.0: test (suite: myproject-test)

Progress: 1/2Test suite not yet implemented

Completed 2 action(s).
ExitSuccess
Type help for available commands. Press enter to force a rebuild.
```

単体テストを書くために、パッケージをいくつか使います。利用したresolverに登録されているパッケージを使うのが一番トラブルが少ないので、その中から使えそうなパッケージを見繕います。パッケージの一覧は Stackage のサイトで確認できます。最新のLTSを使っている場合は、 https://www.stackage.org/lts で一覧を見ることができますので、ここからパッケージを選びます。今回は `HUnit` と `test-framework` と`test-framework-hunit` 、 そして `silently` を利用してみましょう。パッケージを利用するには、 `.cabal` ファイルに依存関係を追加します。 `myproject.cabal` のテストシートのビルド設定に、パッケージを4つ追加してください。

```
test-suite myproject-test
  .. 省略 ..
  build-depends:       base
                     , myproject
                     -- 以下を追加
                     , HUnit
                     , silently
                     , test-framework
                     , test-framework-hunit
```

`.cabal` ファイルを変更すると、 `--file-watch` が変更を捉えて自動的にビルドしてくれます。追加したパッケージは、自動的にHackageからダウンロードされ、ビルドされます。

```console
.. 略 ..

Type help for available commands. Press enter to force a rebuild.
myproject-0.1.0.0: unregistering (missing dependencies: HUnit, silently, test-framework, test-framework-hunit)
hostname-1.0: using precompiled package
mtl-2.2.1: using precompiled package

.. 略 ..

Completed 18 action(s).
ExitSuccess
Type help for available commands. Press enter to force a rebuild.
```

追加したパッケージを使って単体テストを書きます。自分のプログラムでパッケージを利用するには、パッケージに含まれる各モジュールを `import` 文で使うことを宣言します。ここでは、`test/Spec.hs`を以下の内容に書き換えてください。ここではコードの解説はしませんが、プログラミングの経験がある方であればなんとなく何をやっているかは想像できるのではないでしょうか。

```haskell
module Main (main) where

import qualified Lib as Lib
import qualified System.IO.Silently as Silently
import qualified Test.Framework as Test
import qualified Test.Framework.Providers.HUnit as Test
import Test.HUnit ((~=?))

main :: IO ()
main = do
  captured <- Silently.capture_ Lib.someFunc
  Test.defaultMain (Test.hUnitTestToTests ("someFunc" ~=? captured))
```

ファイルを編集すると、 `--file-watch` が自動的に単体テストを走らせてくれます。

```shell
.. 略 ..

myproject-0.1.0.0: test (suite: myproject-test)

: [Failed]1/2
expected: "someFunc"
 but got: "someFunc\n"

         Test Cases  Total
 Passed  0           0
 Failed  1           1
 Total   1           1

Completed 2 action(s).
Test suite failure for package myproject-0.1.0.0
    myproject-test:  exited with: ExitFailure 1
Logs printed to console

Type help for available commands. Press enter to force a rebuild.
```

残念ながらテストが失敗してしまいました。 `Lib.hs` で利用している `putStrLn` は改行コードも出力するようですね。テストの方が誤ってますので、以下のようにテストを直します。

```haskell
  -- .. 省略 ..
  Test.defaultMain (Test.hUnitTestToTests ("someFunc\n" ~=? captured))
```

これでまたテストが通るようになりました。

```
.. 略 ..

myproject-0.1.0.0: test (suite: myproject-test)

: [OK]ss: 1/2

         Test Cases  Total
 Passed  1           1
 Failed  0           0
 Total   1           1

Completed 2 action(s).
ExitSuccess
Type help for available commands. Press enter to force a rebuild.
```