|
| 1 | +--- |
| 2 | +title: HspecでQuickCheckするときもshouldBeなどが使えます |
| 3 | +subHeading: quickcheck-ioパッケージのおかげ |
| 4 | +headingBackgroundImage: ../../img/background.png |
| 5 | +headingDivClass: post-heading |
| 6 | +author: Yuji Yamamoto |
| 7 | +postedBy: <a href="http://the.igreque.info/">Yuji Yamamoto(@igrep)</a> |
| 8 | +date: February 27, 2020 |
| 9 | +tags: |
| 10 | +... |
| 11 | +--- |
| 12 | + |
| 13 | +タイトルがほとんどすべてなんですが詳細を解説します。 |
| 14 | + |
| 15 | +# 📣`shouldBe`などは`property`の中でも使えるので使ってください! |
| 16 | + |
| 17 | +みなさんはHspecでQuickCheckを使ったproperty testを書く際、どのように書いているでしょうか? |
| 18 | +例えば[Hspecのマニュアル](https://hspec.github.io/quickcheck.html)のように、Hspecにproperty testを組み込む例として、次のような例を挙げています。 |
| 19 | + |
| 20 | +```haskell |
| 21 | +describe "read" $ do |
| 22 | + it "is inverse to show" $ property $ |
| 23 | + \x -> (read . show) x == (x :: Int) |
| 24 | +``` |
| 25 | + |
| 26 | +※[こちらのコミット](https://github.com/hspec/hspec/blob/9f3f4c38952f526701a67b6e26336a3a5aec0e89/doc/quickcheck.md)の時点での話です。 |
| 27 | + |
| 28 | +`property`関数に渡した関数<small>(以下、「`porperty`ブロック」と呼びます)</small>の中ではHspecでおなじみの`shouldBe`などのexpectation用関数を使わず、`==`で結果を判定してますよね。 |
| 29 | +このサンプルに倣って、Hspecで書いたテストにproperty testを書くときは、`==`を使ってる方が多いんじゃないでしょうか? |
| 30 | + |
| 31 | +ところが、この記事のタイトルに書いたとおり、実際のところ`property`ブロックの中でも`shouldBe`は利用できます。 |
| 32 | +つまりは、こちら👇のようにも書ける、ということです! |
| 33 | + |
| 34 | +```haskell |
| 35 | +describe "read" $ do |
| 36 | + it "is inverse to show" $ property $ |
| 37 | + \x -> (read . show) x `shouldBe` (x :: Int) |
| 38 | +``` |
| 39 | + |
| 40 | +このように`property`ブロックの中でも`shouldBe`や`shouldSatisfy`といった、Hspec固有のexpectation関数を使うことの利点は、単に構文を他のテストと一貫させることができる、だけではありません。 |
| 41 | +**テストが失敗したときのエラーが分かりやすくなる**、という遥かに重大なメリットがあるのです。 |
| 42 | + |
| 43 | +試しにわざとテストを失敗させてみましょう。 |
| 44 | +先ほどの例: |
| 45 | + |
| 46 | +```haskell |
| 47 | +describe "read" $ do |
| 48 | + it "is inverse to show" $ property $ |
| 49 | + \x -> (read . show) x == (x :: Int) |
| 50 | +``` |
| 51 | + |
| 52 | +における`(x :: Int)`という式を`(x + 1 :: Int)`に変えれば、必ず失敗するはずです。 |
| 53 | + |
| 54 | +```haskell |
| 55 | +describe "read" $ do |
| 56 | + it "is inverse to show" $ property $ |
| 57 | + \x -> (read . show) x == (x + 1 :: Int) |
| 58 | +``` |
| 59 | + |
| 60 | +※お手元で試す場合は[こちら](https://github.com/hspec/hspec/blob/9f3f4c38952f526701a67b6e26336a3a5aec0e89/doc/_includes/QuickCheck.hs)から元のコードを持ってきて、`stack build hspec`なりを実行した上で修正・実行するのが簡単でしょう。 |
| 61 | + |
| 62 | +結果、下記のようなエラーメッセージとなるでしょう。 |
| 63 | + |
| 64 | +``` |
| 65 | +... |
| 66 | + 1) read, when used with ints, is inverse to show |
| 67 | + Falsifiable (after 1 test): |
| 68 | + 0 |
| 69 | +``` |
| 70 | + |
| 71 | +このエラーでは「テストが失敗したこと」と「どんな入力をQuickCheckが生成したか」までしか教えてくれず、わかりづらいですよね。 |
| 72 | + |
| 73 | +一方、`shouldBe`を使用して以下のように書き換えると... |
| 74 | + |
| 75 | +```haskell |
| 76 | +describe "read" $ do |
| 77 | + it "is inverse to show" $ property $ |
| 78 | + \x -> (read . show) x `shouldBe` (x + 1 :: Int) |
| 79 | +``` |
| 80 | + |
| 81 | +エラーメッセージはこう👇なります。 |
| 82 | + |
| 83 | +``` |
| 84 | + 1) read, when used with ints, is inverse to show |
| 85 | + Falsifiable (after 1 test): |
| 86 | + 0 |
| 87 | + expected: 1 |
| 88 | + but got: 0 |
| 89 | +``` |
| 90 | + |
| 91 | +「テストが失敗したこと」と「どんな入力をQuickCheckが生成したか」に加えて、`shouldBe`に与えた両辺の式がどのような値を返したか、まで教えてくれました! |
| 92 | +今回の例は極めて単純なのであまり役に立たないかも知れませんが、あなたが書いた関数をテストするときはやっぱり「期待される結果」と「実際の結果」両方がわかる方がデバッグしやすいですよね! |
| 93 | + |
| 94 | +と、いうわけで今後は`property`関数<small>(あるいはその省略版の`prop`関数)</small>に渡した関数の中でも`shouldBe`などを必ず使ってください! |
| 95 | +<small>(せっかくなんで、今回紹介したドキュメントを[修正するためのPull request](https://github.com/hspec/hspec/pull/429)を送っておきました。これがマージされればこの記事の情報の大半は時代遅れになります)</small> |
| 96 | + |
| 97 | +# 😕なぜ使える? |
| 98 | + |
| 99 | +しかしここで、一つ疑問が残ります。 |
| 100 | +QuickCheckやHspecのドキュメントをつぶさに読んだことがある方はお気づきでしょう。 |
| 101 | +QuickCheckの[`property`関数は、`Testable`という型クラスのメソッド](http://hackage.haskell.org/package/QuickCheck-2.13.2/docs/Test-QuickCheck.html#t:Testable)であるため、`Testable`のインスタンスでなければ使えないはずです。 |
| 102 | +Hspecの`shouldBe`などが返す値は型シノニムのたらい回しをたどればわかるとおり、結局のところ`IO ()`型の値です。 |
| 103 | +ところが`Testable`のインスタンス一覧を見る限り、`IO a`は`Testable`のインスタンスではありません。 |
| 104 | +先ほどの例のように |
| 105 | + |
| 106 | +```haskell |
| 107 | +property $ \x -> (read . show) x `shouldBe` (x + 1 :: Int) |
| 108 | +``` |
| 109 | + |
| 110 | +と書いた場合における、関数型`(a -> prop)`のインスタンスは、`(Arbitrary a, Show a, Testable prop) => Testable (a -> prop)`という定義のとおり、関数の戻り値の型が`Testable`のインスタンスでないと、型チェックを通らないはずです。 |
| 111 | +`Testable`のインスタンスでない、`IO ()`を返しているにも関わらず型エラーが起きなかったのは、一体なぜでしょうか? |
| 112 | + |
| 113 | +その秘密を探るべく、GHCiを立ち上げましょう。 |
| 114 | +先ほどの例のソースコードを`ghci`コマンドに読ませれば、まとめてHspecのモジュールも`import`できるので簡単です。 |
| 115 | + |
| 116 | +```bash |
| 117 | +> stack exec ghci .\QuickCheck.hs |
| 118 | +``` |
| 119 | + |
| 120 | +GHCiが起動したら、`:i Testable`と入力して、`Testable`型クラスのインスタンス一覧を出力しましょう。 |
| 121 | + |
| 122 | +```haskell |
| 123 | +> :i Testable |
| 124 | +class Testable prop where |
| 125 | + property :: prop -> Property |
| 126 | + {-# MINIMAL property #-} |
| 127 | + -- Defined in ‘Test.QuickCheck.Property’ |
| 128 | +instance [safe] Testable Property |
| 129 | + -- Defined in ‘Test.QuickCheck.Property’ |
| 130 | +instance [safe] Testable prop => Testable (Gen prop) |
| 131 | + -- Defined in ‘Test.QuickCheck.Property’ |
| 132 | +instance [safe] Testable Discard |
| 133 | + -- Defined in ‘Test.QuickCheck.Property’ |
| 134 | +instance [safe] Testable Bool |
| 135 | + -- Defined in ‘Test.QuickCheck.Property’ |
| 136 | +instance [safe] (Arbitrary a, Show a, Testable prop) => |
| 137 | + Testable (a -> prop) |
| 138 | + -- Defined in ‘Test.QuickCheck.Property’ |
| 139 | +instance [safe] Testable () |
| 140 | + -- Defined in ‘Test.QuickCheck.Property’ |
| 141 | +instance [safe] Testable Test.HUnit.Lang.Assertion |
| 142 | + -- Defined in ‘Test.QuickCheck.IO’ |
| 143 | +``` |
| 144 | + |
| 145 | +ありました!💡 |
| 146 | +最後の方にある`instance [safe] Testable Test.HUnit.Lang.Assertion`という行に注目してください。 |
| 147 | +[`Test.HUnit.Lang.Assertion`](http://hackage.haskell.org/package/HUnit-1.6.0.0/docs/Test-HUnit-Lang.html#t:Assertion)は、`IO ()`の型シノニムであり、Hspecでも間接的に型シノニムとして参照されています[^hspec-expectation]。 |
| 148 | +要するに`instance [safe] Testable Test.HUnit.Lang.Assertion`という行は`instance [safe] Testable (IO ())`と読み替えることができます<small>(`[safe]`という表記が指しているものについてはここでは省略します!すみません!)</small>。 |
| 149 | + |
| 150 | +[^hspec-expectation]: この節の冒頭で「型シノニムのたらい回し」と呼んだものを追いかけてみましょう。 |
| 151 | +おなじみ[`shouldBe`](http://hackage.haskell.org/package/hspec-expectations-0.8.2/docs/Test-Hspec-Expectations.html#v:shouldBe)は[`Expectation`](http://hackage.haskell.org/package/hspec-expectations-0.8.2/docs/Test-Hspec-Expectations.html#t:Expectation)という型の値を返します。 |
| 152 | +そして`Expectation`は`Assertion`の型シノニムであり、クリックすると[`Test.HUnit.Lang.Assertion`](http://hackage.haskell.org/package/HUnit-1.6.0.0/docs/Test-HUnit-Lang.html#t:Assertion)であることがわかります。 |
| 153 | +そして`Assertion`はそう、`type Assertion = IO ()`とあるとおり`IO ()`なのです。やっと知ってる型にたどり着きました😌。 |
| 154 | + |
| 155 | +紹介したとおり`Testable`のドキュメントには`Testable Assertion`なんて記載はありませんし、じゃあ一体どこで定義したのか、というとそう、続く行に`-- Defined in ‘Test.QuickCheck.IO’`と書かれているとおり、[`Test.QuickCheck.IO`](https://hackage.haskell.org/package/quickcheck-io-0.2.0/docs/Test-QuickCheck-IO.html)というモジュールで定義されています! |
| 156 | + |
| 157 | +`Test.QuickCheck.IO`は、名前のとおりQuickCheckの`Testable`について、`IO`のorphan instanceを定義するためのモジュールです。 |
| 158 | +これを[`import`している](https://github.com/hspec/hspec/blob/226510631f24b674827e99d17d10f9f92440c5a9/hspec-core/src/Test/Hspec/Core/QuickCheckUtil.hs#L18)が故に、Hspecでは`property`ブロックの中で`shouldBe`などが利用できるんですね! |
| 159 | + |
| 160 | +結論: |
| 161 | + |
| 162 | +- orphan instanceわかりづらい😥 |
| 163 | +- GHCiの`:i`はorphan instanceであろうとインスタンスを定義した箇所を見つけてくれるから便利! |
0 commit comments