HSpec と ghcid でHaskellの快適なTDD環境を構築する

## 前書き

Haskellのプロジェクトで、コードの変更を検知して自動でテストを再実行できるようになるまでの手順を、新しくプロジェクトを作るところから解説します。

Haskellのテストフレームワークである[HSpec](https://github.com/hspec/hspec)と、GHCi(HaskellのREPL)ベースのツールである[ghcid](https://github.com/ndmitchell/ghcid)を使います。

[stack](https://docs.haskellstack.org/en/stable/README/)がインストールされていることを前提としているので、まだの場合はまずstackをインストールしてください。
stackや周辺ツールについては、[本気で Haskell したい人向けの Stack チュートリアル](https://qiita.com/waddlaw/items/49874f4cf9b680e4b015)で非常に丁寧にまとめられています。

## 手順

### 新規プロジェクトの作成

まずは `stack new` します。

```
$ stack new project
$ cd project
```

### HSpecの導入

`package.yaml` を編集し、HSpecを導入します。

```diff:package.yaml
tests:
  project-test:
    main:                Spec.hs
    source-dirs:         test
    ghc-options:
    - -threaded
    - -rtsopts
    - -with-rtsopts=-N
    dependencies:
    - project
+   - hspec
```

次に `test/Spec.hs` を編集します。
ファイルの内容は1行だけになります。

```diff:test/Spec.hs
- main :: IO ()
- main = putStrLn "Test suite not yet implemented"
+ {-# OPTIONS_GHC -F -pgmF hspec-discover #-}
```

※ここまでの設定は `stack new project hspec` と [HSpec 用のテンプレート](https://github.com/commercialhaskell/stack-templates/blob/master/hspec.hsfiles)を指定してプロジェクトを作成することで省略できます。

`hspec-discover` は命名規則に従ってコードに対応するSpecファイルを見つけ、テストの実行対象に含めてくれるツールです。
利用にはインストールが必要なので、やっておきましょう。

```
$ stack install hspec-discover
```

この状態で `stack test` を実行すれば、 `0 examples, 0 failures` という結果が得られるはずです。

### テストの作成

`src/Some.hs` というファイルに適当な関数を作ってテストします。

```Haskell:src/Some.hs
module Some where

fizzbuzz :: Int -> String
fizzbuzz num
  | num `mod` 15 == 0 = "FizzBuzz"
  | num `mod` 3 == 0 = "Fizz"
  | num `mod` 5 == 0 = "Buzz"
  | otherwise = show num
```

これをテストするコードは、 `test/SomeSpec.hs` というファイルに置きます。
ファイルが `src/Xxx/Yyy.hs` だった場合は `test/Xxx/YyySpec.hs` という対応関係になります。

```Haskell:test/SomeSpec.hs
module SomeSpec (spec) where

import Test.Hspec
import Some

spec :: Spec
spec = do
  describe "fizzbuzz" $ do
    it "数値を文字列にして返す" $ do
      fizzbuzz 1 `shouldBe` "1"
      fizzbuzz 2 `shouldBe` "2"

    it "3の倍数の場合はFizzを返す" $ do
      fizzbuzz 3 `shouldBe` "Fizz"
      fizzbuzz 6 `shouldBe` "Fizz"

    it "5の倍数の場合はBuzzを返す" $ do
      fizzbuzz 5 `shouldBe` "Buzz"
      fizzbuzz 10 `shouldBe` "Buzz"

    it "3と5両方の倍数の場合はFizzBuzzを返す" $ do
      fizzbuzz 15 `shouldBe` "FizzBuzz"
      fizzbuzz 30 `shouldBe` "FizzBuzz"
```

再度 `stack test` を実行して以下のような結果になれば成功です。

```
Some
  fizzbuzz
    数値を文字列にして返す
    3の倍数の場合はFizzを返す
    5の倍数の場合はBuzzを返す
    3と5両方の倍数の場合はFizzBuzzを返す

Finished in 0.0020 seconds
4 examples, 0 failures
```

### ghcidの導入

まずはghcidをインストールします。

```
$ stack install ghcid
```

インストールできたら、以下のコマンドを叩くだけで完成です。

```
$ ghcid --command="stack ghci src/*.hs test/Spec.hs" --test="main"
```

実際に `src/Some.hs` や `test/SomeSpec.hs` を変更して、テストが自動的に再実行されることを確かめてみてください。

ちなみに、 `.ghcid` ファイルにオプションの内容を書いておくと、 `ghcid` コマンドを叩くだけで起動できるので便利です。

```:.ghcid
--command="stack ghci src/*.hs test/Spec.hs"
--test="main"
```

### おまけ

これだけで十分なのですが、個人的な趣味で[Liquid Haskell](https://github.com/ucsd-progsys/liquidhaskell)を導入した場合についても紹介します。Haskellを始めたばかりの人はあまり気にしなくて良いです。

GHCiからLiquid Haskellを実行するために、まず `package.yaml` を編集します。

```diff:package.yaml
  description:         Please see the README on GitHub at <https://github.com/githubuser/hello-haskell#readme>
  
  dependencies:
  - base >= 4.7 && < 5
+ - liquidhaskell
```

Liquid HaskellはStackageに登録されていないので、これに加えて `stack.yaml` にも記載が必要です。

```diff:stack.yaml
  # extra-deps: []
+ extra-deps:
+ - git: https://github.com/ucsd-progsys/liquidhaskell.git
+   commit: 0d88f4c30837232592603f16835c300fc61026f6
+ - git: https://github.com/ucsd-progsys/liquid-fixpoint.git
+   commit: 42c027ab9ae47907c588a2f1f9c05a5e0aa881e9
```

ここに書かれたコミットIDは筆者が試した時点のものなので、自分で試す場合はLiquid HaskellのGitHubから最新のコミットIDを参照して書き換えてください。

この状態で `stack build` を走らせると、stackが足りない設定を教えてくれるので、それに従って更に `stack.yaml` に追記します。
例として筆者が試した時点のものを記載しておきます。

```diff:stack.yaml
  # extra-deps: []
  extra-deps:
  - git: https://github.com/ucsd-progsys/liquidhaskell.git
    commit: 0d88f4c30837232592603f16835c300fc61026f6
  - git: https://github.com/ucsd-progsys/liquid-fixpoint.git
    commit: 42c027ab9ae47907c588a2f1f9c05a5e0aa881e9
+ - fgl-visualize-0.1.0.1@sha256:e682066053a6e75478a08fd6822dd0143a3b8ea23244bdb01dd389a266447c5e
+ - located-base-0.1.1.1@sha256:7c6395f2b6fbf2d5f76c3514f774423838c0ea94e1c6a5530dd3c94b30c9d1c8
```

再度 `stack build` を走らせてビルドが通れば成功です。
更に依存関係を追加しろとstackに言われることもあるので、ビルドが始まるまで `stack.yaml` に追加 -> `stack build` を繰り返しましょう。
最初のビルドが終わるまでは結構時間がかかると思います。

ビルドが成功したら、GHCiからもLiquid Haskellが使えるようになっているはずです。
https://github.com/ucsd-progsys/liquidhaskell#how-to-run-inside-ghci にコマンド例が載っているので、試してください。

```
$ stack ghci liquidhaskell
ghci> :m +Language.Haskell.Liquid.Liquid
ghci> liquid ["app/Main.hs"]
```

Liquid Haskellがちゃんと動いたら、ghcidで変更検知と再実行ができるようにします。

```
$ ghcid --command="stack ghci hello-haskell:lib --ghci-options -ghci-script=.ghci --ghci-options=-fobject-code" --setup=":m +Language.Haskell.Liquid.Liquid" --test="liquid [\"app/Main.hs\"]"
```

以上です。
Haskellでも快適なTDDライフが送れるようになれば幸いです。