Hspecベストプラクティス

Hspecベストプラクティス

Haskell Advent Calendar 2015 の14日目のエントリーです。

イントロ

HspecHaskellコミュニティにすっかり受け入れられたテスティングフレームワークと言ってよいかと思います。しかし!まだ確たるベストプラクティスは無いと言えるのではないでしょうか!

Hspecの元となったRSpecRubyコミュニティでデファクト・スタンダードとなっており、ベストプラクティスの蓄積があります。これを、Haskellの事情を鑑みつつ適用してみようという記事です。

ではないので、その辺りは別の記事を読んでください。

あと、オフィシャルのexampleがあるので、これに先に目を通しておくと話が早いかもしれません。

気になる点、賛同できないプラクティス、改善点などありましたら、是非コメントで教えてください。

では始めます!

一般的なディレクトリ構造に従う

  • 一つのソースファイルに一つのスペック
  • ファイル名はSpec.hsで終える
  • ソースコードのディレクトリ構造に対応させる

が慣用的です。 例えば src/Foo.hsのSpecはtest/FooSpec.hssrc/Data/Bar.hstest/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さんもテストの抽象化には反対していました。

とはいえ現実世界では、読みやすい抽象化をすればいいのでは?とか、さすがにボイラープレートコードが多すぎる、とか、悩ましい状況は発生します。適度にルールを破る分には良いかと。

マッチャーを知る

shouldBeshouldReturnなどをマッチャーと言います。これはそんなに数多くないので、一度目を通すとよいです。 ドキュメントはこちら にあります*1

GHCIでテストする

module FooSpec (main, spec) where

main :: IO ()
main = hspec spec

spec :: Spec
spec = describe "Foo.bar" $ do
  -- 省略

というようにmainspecを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の人になってしまいました。悲しいですね!

リンク集

本家

この記事の元ネタ

RSpecについて:

Hspecについて

*1:ちなみに中置記法があるおかげでRSpecのようにshould/expect論争がないです。