Haskell
HSpec
ghcid

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


前書き

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

HaskellのテストフレームワークであるHSpecと、GHCi(HaskellのREPL)ベースのツールであるghcidを使います。

stackがインストールされていることを前提としているので、まだの場合はまずstackをインストールしてください。

stackや周辺ツールについては、本気で Haskell したい人向けの Stack チュートリアルで非常に丁寧にまとめられています。


手順


新規プロジェクトの作成

まずは stack new します。

$ stack new project

$ cd project


HSpecの導入

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


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行だけになります。


test/Spec.hs

- main :: IO ()

- main = putStrLn "Test suite not yet implemented"
+ {-# OPTIONS_GHC -F -pgmF hspec-discover #-}

hspec-discover は命名規則に従ってコードに対応するSpecファイルを見つけ、テストの実行対象に含めてくれるツールです。

利用にはインストールが必要なので、やっておきましょう。

$ stack install hspec-discover

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


テストの作成

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


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 という対応関係になります。


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.hstest/SomeSpec.hs を変更して、テストが自動的に再実行されることを確かめてみてください。

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


.ghcid

--command="stack ghci src/*.hs test/Spec.hs"

--test="main"


おまけ

これだけで十分なのですが、個人的な趣味でLiquid Haskellを導入した場合についても紹介します。Haskellを始めたばかりの人はあまり気にしなくて良いです。

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


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 にも記載が必要です。


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 に追記します。

例として筆者が試した時点のものを記載しておきます。


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ライフが送れるようになれば幸いです。