Hspecベストプラクティス
Hspecベストプラクティス
Haskell Advent Calendar 2015 の14日目のエントリーです。
イントロ
Hspec はHaskellコミュニティにすっかり受け入れられたテスティングフレームワークと言ってよいかと思います。しかし!まだ確たるベストプラクティスは無いと言えるのではないでしょうか!
Hspecの元となったRSpecはRubyコミュニティでデファクト・スタンダードとなっており、ベストプラクティスの蓄積があります。これを、Haskellの事情を鑑みつつ適用してみようという記事です。
ではないので、その辺りは別の記事を読んでください。
あと、オフィシャルのexampleがあるので、これに先に目を通しておくと話が早いかもしれません。
気になる点、賛同できないプラクティス、改善点などありましたら、是非コメントで教えてください。
では始めます!
一般的なディレクトリ構造に従う
- 一つのソースファイルに一つのスペック
- ファイル名は
Spec.hs
で終える - ソースコードのディレクトリ構造に対応させる
が慣用的です。 例えば src/Foo.hs
のSpecはtest/FooSpec.hs
、src/Data/Bar.hs
はtest/Data/BarSpec.hs
となります。
hspec-discoverでテストスイートをまとめる
複数あるテストを一つのテストスイートにまとめるのは結構面倒です。
上記の命名規則(Spec.hs
でファイル名が終わる)に従い、各ファイルでspec :: Spec
をエクスポートしておけば、hspec-discoverが規約に沿ったファイルから自動的にテストスイートを作ってくれます。
下記のようにファイルを作っておけば $ stack test
できるはず。
test/Spec.hs
:
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
foo.cabal
:
test-suite foo-test main-is: Spec.hs
それぞれの関数について、振る舞いの説明をする
describe
で説明する関数を宣言し、it
でその振る舞いを説明します。
例:
describe "Foo.bar" $ it "should return multiple of given number" $ do Foo.bar 4 `shouldBe` 8
context
を活用する
context
に文脈を書くと整理されたテストスイートになります。
describe "Foo.baz" $ context "when [] was given" $ do it "should return Nothing" $ do Foo.baz [] `shouldBe` Nothing context "when [1] was given" $ do it "should return Just 1" $ do Foo.baz [1] `shouldBe` Just 1
一つのテストで一つの振る舞いを確認する
複数の振る舞いを確認すると失敗するテストを見つけるのが難しくなります。
テストが遅くなりすぎる場合は複数の振る舞いを検証するのもアリです。
Good:
describe "Logger.log" $ it "should write log to log file" $ do -- 省略 it "should write log to stdout" $ do -- 省略
Bad:
describe "Logger.log" $ it "should write log to log file and stdout" $ do -- 省略
テストをあまり抽象化しない
BDDはソフトウェアの振る舞いを記述するための手法です。多少冗長でも、記述された振る舞いの読みやすさを優先したほうがよいです。
ちょっといい例が思いつかないので、瑣末な例で。
describe "Foo.baz" $ do forM_ ["cat", "dog", "snake"] $ \name -> do context "with '" ++ name ++ "'" $ do it "should return " ++ name ++ "-chan" $ do -- 省略
は
describe "Foo.baz" $ do context "with 'cat'" $ do it "should return cat-chan" $ do -- 省略 context "with 'dog'" $ do it "should return dog-chan" $ do -- 省略 context "with 'snake'" $ do it "should return snake-chan" $ do -- 省略
としておいた方がよいです。前者の例はコードから振る舞いを読み取れません。BDDのテストコードは抽象化の対象ではなくソフトウェアの振る舞いを記述するものである、ということを心に留めておきましょう。
今年の24 days of HackageでHspecが取り上げられており、コメント欄でYesodやPersistentのメンテナーであるGregさんもテストの抽象化には反対していました。
とはいえ現実世界では、読みやすい抽象化をすればいいのでは?とか、さすがにボイラープレートコードが多すぎる、とか、悩ましい状況は発生します。適度にルールを破る分には良いかと。
マッチャーを知る
shouldBe
、shouldReturn
などをマッチャーと言います。これはそんなに数多くないので、一度目を通すとよいです。
ドキュメントはこちら にあります*1。
GHCIでテストする
module FooSpec (main, spec) where main :: IO () main = hspec spec spec :: Spec spec = describe "Foo.bar" $ do -- 省略
というようにmain
とspec
をexposeしておくと、
$ stack ghci λ import qualified FooSpec λ FooSpec.main Foo.bar should return something Foo.baz should do something
という具合にGHCiの中でテストを実行できます。$ stack test
より GHCiの中でreload!
するほうが早いのでオススメ。
便利なライブラリがある
一時ディレクトリ、一時ファイルの利用はMockeryが便利です。またSilentlyを使うと標準入出力をキャプチャできます。
WAIにはhspec-wai、Snapにはhspec-snapがあります。ちなみに僕はhspec-waiの作者の一人です。
まとめというか所感
Hspecいいソフトウェアだと思います。型がリッチな言語とふわっと振る舞いからコードを育てるBDDの相性がめちゃくちゃ良い。doctestと併用して、(Type|Test|Behaviour) Driven Developmentは捗ります。
あと、他にもRSpecのベストプラクティスをパクれるところは沢山あると思うので、RubyわかるHaskeller諸氏は是非参考にしてみてください。ちなみにRSpecを長年メンテしてたDavid ChelimskyさんはClojureの人になってしまいました。悲しいですね!
リンク集
本家
この記事の元ネタ
- http://betterspecs.org/jp/
- http://magazine.rubyist.net/?0032-TranslationArticle
- http://www.methodsandtools.com/tools/rspecbestpractices.php
RSpecについて:
Hspecについて
- https://www.reddit.com/r/haskell/comments/3vaj0z/24_days_of_hackage_2015_day_3_hspec_the/
- http://conscientiousprogrammer.com/blog/2015/12/03/24-days-of-hackage-2015-day-3-hspec-the-importance-of-testing/
- http://looprecur.com/blog/testing-a-web-application-with-hspec/
- http://alpmestan.com/posts/2014-06-18-testing-attoparsec-parsers-with-hspec.html
- http://d.hatena.ne.jp/kazu-yamamoto/20121205/1354692144
- http://mike-neck.hatenadiary.com/entry/2015/06/19/103536
- http://oropon.hatenablog.com/entry/2014/03/14/012729