タイトルがほとんどすべてなんですが詳細を解説します。
Link to
here📣shouldBe
などはproperty
の中でも使えるので使ってください!
みなさんはHspecでQuickCheckを使ったproperty testを書く際、どのように書いているでしょうか?
例えばHspecのマニュアルのように、Hspecにproperty testを組み込む例として、次のような例を挙げています。
"read" $ do
describe "is inverse to show" $ property $
it -> (read . show) x == (x :: Int) \x
※こちらのコミットの時点での話です。
property
関数に渡した関数(以下、「porperty
ブロック」と呼びます)の中ではHspecでおなじみのshouldBe
などのexpectation用関数を使わず、==
で結果を判定してますよね。
このサンプルに倣って、Hspecで書いたテストにproperty testを書くときは、==
を使ってる方が多いんじゃないでしょうか?
ところが、この記事のタイトルに書いたとおり、実際のところproperty
ブロックの中でもshouldBe
は利用できます。
つまりは、こちら👇のようにも書ける、ということです!
"read" $ do
describe "is inverse to show" $ property $
it -> (read . show) x `shouldBe` (x :: Int) \x
このようにproperty
ブロックの中でもshouldBe
やshouldSatisfy
といった、Hspec固有のexpectation関数を使うことの利点は、単に構文を他のテストと一貫させることができる、だけではありません。
テストが失敗したときのエラーが分かりやすくなる、という遥かに重大なメリットがあるのです。
試しにわざとテストを失敗させてみましょう。
先ほどの例:
"read" $ do
describe "is inverse to show" $ property $
it -> (read . show) x == (x :: Int) \x
における(x :: Int)
という式を(x + 1 :: Int)
に変えれば、必ず失敗するはずです。
"read" $ do
describe "is inverse to show" $ property $
it -> (read . show) x == (x + 1 :: Int) \x
※お手元で試す場合はこちらから元のコードを持ってきて、stack build hspec
なりを実行した上で修正・実行するのが簡単でしょう。
結果、下記のようなエラーメッセージとなるでしょう。
...
1) read, when used with ints, is inverse to show
Falsifiable (after 1 test):
0
このエラーでは「テストが失敗したこと」と「どんな入力をQuickCheckが生成したか」までしか教えてくれず、わかりづらいですよね。
一方、shouldBe
を使用して以下のように書き換えると…
"read" $ do
describe "is inverse to show" $ property $
it -> (read . show) x `shouldBe` (x + 1 :: Int) \x
エラーメッセージはこう👇なります。
1) read, when used with ints, is inverse to show
Falsifiable (after 1 test):
0
expected: 1
but got: 0
「テストが失敗したこと」と「どんな入力をQuickCheckが生成したか」に加えて、shouldBe
に与えた両辺の式がどのような値を返したか、まで教えてくれました!
今回の例は極めて単純なのであまり役に立たないかも知れませんが、あなたが書いた関数をテストするときはやっぱり「期待される結果」と「実際の結果」両方がわかる方がデバッグしやすいですよね!
と、いうわけで今後はproperty
関数(あるいはその省略版のprop
関数)に渡した関数の中でもshouldBe
などを必ず使ってください!
(せっかくなんで、今回紹介したドキュメントを修正するためのPull requestを送っておきました。これがマージされればこの記事の情報の大半は時代遅れになります)
Link to
here😕なぜ使える?
しかしここで、一つ疑問が残ります。
QuickCheckやHspecのドキュメントをつぶさに読んだことがある方はお気づきでしょう。
QuickCheckのproperty
関数は、Testable
という型クラスのメソッドであるため、Testable
のインスタンスでなければ使えないはずです。
HspecのshouldBe
などが返す値は型シノニムのたらい回しをたどればわかるとおり、結局のところIO ()
型の値です。
ところがTestable
のインスタンス一覧を見る限り、IO a
はTestable
のインスタンスではありません。
先ほどの例のように
$ \x -> (read . show) x `shouldBe` (x + 1 :: Int) property
と書いた場合における、関数型(a -> prop)
のインスタンスは、(Arbitrary a, Show a, Testable prop) => Testable (a -> prop)
という定義のとおり、関数の戻り値の型がTestable
のインスタンスでないと、型チェックを通らないはずです。
Testable
のインスタンスでない、IO ()
を返しているにも関わらず型エラーが起きなかったのは、一体なぜでしょうか?
その秘密を探るべく、GHCiを立ち上げましょう。
先ほどの例のソースコードをghci
コマンドに読ませれば、まとめてHspecのモジュールもimport
できるので簡単です。
> stack exec ghci .\QuickCheck.hs
GHCiが起動したら、:i Testable
と入力して、Testable
型クラスのインスタンス一覧を出力しましょう。
> :i Testable
class Testable prop where
property :: prop -> Property
{-# MINIMAL property #-}
-- Defined in ‘Test.QuickCheck.Property’
instance [safe] Testable Property
-- Defined in ‘Test.QuickCheck.Property’
instance [safe] Testable prop => Testable (Gen prop)
-- Defined in ‘Test.QuickCheck.Property’
instance [safe] Testable Discard
-- Defined in ‘Test.QuickCheck.Property’
instance [safe] Testable Bool
-- Defined in ‘Test.QuickCheck.Property’
instance [safe] (Arbitrary a, Show a, Testable prop) =>
Testable (a -> prop)
-- Defined in ‘Test.QuickCheck.Property’
instance [safe] Testable ()
-- Defined in ‘Test.QuickCheck.Property’
instance [safe] Testable Test.HUnit.Lang.Assertion
-- Defined in ‘Test.QuickCheck.IO’
ありました!💡
最後の方にあるinstance [safe] Testable Test.HUnit.Lang.Assertion
という行に注目してください。
Test.HUnit.Lang.Assertion
は、IO ()
の型シノニムであり、Hspecでも間接的に型シノニムとして参照されています1。
要するにinstance [safe] Testable Test.HUnit.Lang.Assertion
という行はinstance [safe] Testable (IO ())
と読み替えることができます([safe]
という表記が指しているものについてはここでは省略します!すみません!)。
紹介したとおりTestable
のドキュメントにはTestable Assertion
なんて記載はありませんし、じゃあ一体どこで定義したのか、というとそう、続く行に-- Defined in ‘Test.QuickCheck.IO’
と書かれているとおり、Test.QuickCheck.IO
というモジュールで定義されています!
Test.QuickCheck.IO
は、名前のとおりQuickCheckのTestable
について、IO
のorphan instanceを定義するためのモジュールです。
これをimport
しているが故に、Hspecではproperty
ブロックの中でshouldBe
などが利用できるんですね!
結論:
- orphan instanceわかりづらい😥
- GHCiの
:i
はorphan instanceであろうとインスタンスを定義した箇所を見つけてくれるから便利!
この節の冒頭で「型シノニムのたらい回し」と呼んだものを追いかけてみましょう。
おなじみshouldBe
はExpectation
という型の値を返します。
そしてExpectation
はAssertion
の型シノニムであり、クリックするとTest.HUnit.Lang.Assertion
であることがわかります。
そしてAssertion
はそう、type Assertion = IO ()
とあるとおりIO ()
なのです。やっと知ってる型にたどり着きました😌。↩︎