テストの傾向を分析する
ここまでは単にテストが成功/失敗するという例だけを見て,テストの記述や生成する値の変更を行ってきました。単純な例ではこれでも十分かもしれませんが,実際のテストではより詳細な分析が必要になります。そこで,生成された値を分析する方法を説明します。
一番単純な方法は,生成された値を全部出力してしまうことです。
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.