前書き
Haskellのプロジェクトで、コードの変更を検知して自動でテストを再実行できるようになるまでの手順を、新しくプロジェクトを作るところから解説します。
HaskellのテストフレームワークであるHSpecと、GHCi(HaskellのREPL)ベースのツールであるghcidを使います。
stackがインストールされていることを前提としているので、まだの場合はまずstackをインストールしてください。
stackや周辺ツールについては、本気で Haskell したい人向けの Stack チュートリアルで非常に丁寧にまとめられています。
手順
新規プロジェクトの作成
まずは stack new
します。
$ stack new project
$ cd project
HSpecの導入
package.yaml
を編集し、HSpecを導入します。
tests:
project-test:
main: Spec.hs
source-dirs: test
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- project
+ - hspec
次に test/Spec.hs
を編集します。
ファイルの内容は1行だけになります。
- main :: IO ()
- main = putStrLn "Test suite not yet implemented"
+ {-# OPTIONS_GHC -F -pgmF hspec-discover #-}
※ここまでの設定は stack new project hspec
と HSpec 用のテンプレートを指定してプロジェクトを作成することで省略できます。
hspec-discover
は命名規則に従ってコードに対応するSpecファイルを見つけ、テストの実行対象に含めてくれるツールです。
利用にはインストールが必要なので、やっておきましょう。
$ stack install hspec-discover
この状態で stack test
を実行すれば、 0 examples, 0 failures
という結果が得られるはずです。
テストの作成
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
という対応関係になります。
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
コマンドを叩くだけで起動できるので便利です。
--command="stack ghci src/*.hs test/Spec.hs"
--test="main"
おまけ
これだけで十分なのですが、個人的な趣味でLiquid Haskellを導入した場合についても紹介します。Haskellを始めたばかりの人はあまり気にしなくて良いです。
GHCiからLiquid Haskellを実行するために、まず 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
にも記載が必要です。
# 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
に追記します。
例として筆者が試した時点のものを記載しておきます。
# 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ライフが送れるようになれば幸いです。