テストの傾向を分析する

 ここまでは単にテストが成功/失敗するという例だけを見て,テストの記述や生成する値の変更を行ってきました。単純な例ではこれでも十分かもしれませんが,実際のテストではより詳細な分析が必要になります。そこで,生成された値を分析する方法を説明します。

 一番単純な方法は,生成された値を全部出力してしまうことです。

 QuickCheckは,このための機能としてverboseCheckという関数を提供しています。quickCheck関数の代わりにverboseCheckを使用することで,生成された値をすべて出力します。

Prelude Test.QuickCheck> :t verboseCheck
verboseCheck :: (Testable a) => a -> IO ()

Prelude Test.QuickCheck> verboseCheck $ \x -> let types = (x::Integer) in x > 10 ==> x < x+1
~ 略 ~
0:
1
0:
1
0:
-2
0:
0
0:
0
Arguments exhausted after 0 tests.
Prelude Test.QuickCheck> verboseCheck $ \x -> let types = (x::Integer) in x < x+1
0:
0
1:
-2
2:
0
3:
-3
4:
0
5:
1
~ 略 ~
90:
-24
91:
9
92:
-37
93:
20
94:
-5
95:
0
96:
17
97:
11
98:
35
99:
4
OK, passed 100 tests.

 先に説明した通り,生成される数値の範囲が徐々に広がっていることがわかりますね。同様にリストのようなデータ構造でも,徐々に大きなものを生成していくことがわかります。

*QuickSort> verboseCheck prop_Quicksort
0:
[_1]
1:
[_2,_3]
2:
[_1,_4,_3,_3]
3:
[_3]
4:
[]
5:
[]
~ 略 ~
95:
[_5,_16,_29,_3,_4,_4,_4,_27,_15]
96:
[_41,_40,_11,_31,_38,_40,_34,_41,_42,_34,_45,_5,_44,_38,_27,_17,_37,_28,_29,_41,
_17,_1,_43,_19,_41,_4,_23,_17,_17,_24,_6,_37,_28,_44,_42,_25,_32,_34,_6,_18,_30,
_40,_34,_9,_12]
97:
[_5,_22,_21,_13]
98:
[_18,_32,_33,_3,_29,_10,_31,_24,_33,_28,_27,_26,_31,_34,_3,_9,_5,_31,_19,_33]
99:
[_26,_8,_1,_10,_13,_4,_3,_23,_19,_15,_14,_11]
OK, passed 100 tests.

 こうした長い出力だと,本当に見たかった情報が埋もれてしまう危険性があります。すべての値を直接出力するのではなく,使用した値から傾向を取り出す方法はないでしょうか?

 QuickCheckは,こうした傾向分析のための関数をいくつか用意しています。例えば,テストの中から「成功するのが当たり前である値の含まれる割合」を取り出すには,trivial(「自明な」という意味)関数を使います。

 空リストの場合,リストの中身がないので,quicksort関数を適用しても適用しなくても結果は同じです。また,リストの要素が一つしかない場合にも同じことがいえます。そこでこれらの場合を自明なものとして,他のものと区別してみることにしましょう。

*QuickSort> :t trivial
trivial :: (Testable a) => Bool -> a -> Property
*QuickSort> quickCheck $ \xs -> null xs `trivial` prop_Quicksort xs
OK, passed 100 tests (18% trivial).
*QuickSort> quickCheck $ \xs -> (length xs == 1) `trivial` prop_Quicksort xs
OK, passed 100 tests (11% trivial).
*QuickSort> quickCheck $ \xs -> (null xs || (length xs == 1)) `trivial` prop_Quicksort xs
OK, passed 100 tests (29% trivial).
*QuickSort> quickCheck $ \xs -> (length xs <= 1) `trivial` prop_Quicksort xs
OK, passed 100 tests (33% trivial).

 実は生成された値のうち,かなりの割合で自明なものが含まれていたことがわかります。

 さて,値の傾向分析を行う時,対象としたいのは「自明な場合」の割合だけではないでしょう。「ある性質を持つものはどのくらいあるのか」といった様々な場合の割合について調べるのが普通だと思います。

 こうした目的のために,QuickCheckでは様々な場合を区別する汎用的な関数として,classifyを用意しています。

Prelude Test.QuickCheck> :i classify
classify :: (Testable a) => Bool -> String -> a -> Property
        -- Defined in Test.QuickCheck
infix 1 classify

 classifyは,第1引数の条件式がTrueである場合の割合を第2引数の文字列とともに出力します。

prop_QuicksortWithClassify xs
    = null xs `classify` "list is null"
    $ (length xs == 1) `classify` "list has only one element"
    $ (length xs <= 10) `classify` "list is short"
    $ prop_Quicksort xs

*QuickSort> quickCheck prop_QuicksortWithClassify
OK, passed 100 tests.
46% list is short.
22% list is null, list is short.
12% list has only one element, list is short.

 それぞれの割合が表示されているのに加え,複数の条件に当てはまる場合にそれぞれのメッセージを合成しているのがわかります。classifyとtrivialを組み合わせて使うことで,複数の「自明な場合」を区別できます。

prop_QuicksortWithClassify' xs
    = (length xs <= 1) `trivial` prop_QuicksortWithClassify xs

*QuickSort> quickCheck prop_QuicksortWithClassify'
OK, passed 100 tests.
42% list is short.
28% trivial, list is null, list is short.
11% trivial, list has only one element, list is short.

 また,条件式や文字列の代わりに値そのものを与えるcollectも用意されています。当然ながら,渡す値はShowクラスのインスタンスでなければなりません。

*QuickSort> :t collect
collect :: (Testable b, Show a) => a -> b -> Property
*QuickSort> quickCheck $ \xs -> collect (length xs) $ prop_Quicksort xs
OK, passed 100 tests.
21% 1.
17% 0.
8% 2.
6% 6.
6% 3.
6% 10.
4% 8.
4% 4.
3% 9.
3% 5.
3% 18.
3% 13.
3% 12.
2% 7.
2% 19.
2% 16.
2% 14.
1% 36.
1% 23.
1% 20.
1% 17.
1% 15.