Skip to content

Commit 04b06a9

Browse files
authored
Merge pull request #187 from haskell-jp/property-io
記事の追加: HspecでQuickCheckするときもshouldBeなどが使えます
2 parents 8ab3870 + 2f19a18 commit 04b06a9

File tree

1 file changed

+163
-0
lines changed

1 file changed

+163
-0
lines changed
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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

Comments
 (0)