Haskellでは多くの場合,データ駆動型のテスト・ツールであるQuickCheckの方が,xUnitの一つであるHUnitより役立ちます。
ただ,QuickCheckは万能ではありません。場合によってはHUnitの方が有効なこともあります。今回は「QuickCheckでこうしたことはできるのか?」という疑問に一つひとつ答えていきたいと思います。
高階関数をテストできるか?
まず「関数を値に取る関数」,いわゆる高階関数をテストする場合を考えましょう。高階関数((a -> c) -> b)をテストするには,引数になる関数(a -> c)がArbitraryクラスとShowクラスのインスタンスである必要があります。
Prelude Test.QuickCheck> :t quickCheck quickCheck :: (Testable a) => a -> IO () Prelude Test.QuickCheck> :i Testable class Testable a where property :: a -> Property -- Defined in Test.QuickCheck instance Testable () -- Defined in Test.QuickCheck instance Testable Bool -- Defined in Test.QuickCheck instance Testable Result -- Defined in Test.QuickCheck instance Testable Property -- Defined in Test.QuickCheck instance (Arbitrary a, Show a, Testable b) => Testable (a -> b) -- Defined in Test.QuickCheck
問題は「関数をShowクラスのインスタンスとして定義する」ということがが何を意味するのかがはっきりしないことです。関数の何を表示すれば十分な表現だといえるのでしょうか?
すぐに思いつくのは,入力と出力の対を表示することです。しかし,こうした表現が有効なのは,生成する関数の種類がある程度わかっている場合に限られます。QuickCheckのように型が一致する関数をランダムに生成する場合には意味のない表示です。
ただ「値が関数であること」だけを示す表示はできます。例えば,baseパッケージのText.Show.Functionsモジュールでは以下のような形でインスタンスが定義されています。
instance Show (a -> b) where showsPrec _ _ = showString "<function>"
関数を<function>という文字列にするというだけの定義です。失敗しても他のテストでの反例に相当する意味のある情報は表示されません。
Prelude Test.QuickCheck Text.Show.Functions> quickCheck $ \f x -> let types = (x::Int, f::Int -> Int) in f x == f (f x) Falsifiable, after 2 tests: <function> -1 Prelude Test.QuickCheck Text.Show.Functions> quickCheck $ \f x -> let types = (x::Int, f::Int -> Int) in f x == f (f x) Falsifiable, after 0 tests: <function> -2
実際には,Arbitraryクラスによって自動生成される関数自体,あまり意味のないものです。様々な型とその値の組み合わせは無限にあり,テスト対象になる関数のリストをあらかじめ渡しておくことはできないからです。
高階関数をテストする場合には,高階関数のテストだけに専念し,他のテストとは明確に区別しましょう。テストに複数の目的が混ざっていると,テストのどこに問題があるのを探し当てるのが困難になります。高階関数のテストに限らず,QuickCheckによるテストは,性質(property)ごとに行うようにしましょう。
いくつか例を見ましょう。
prop_MapCompose f g xs = fmap (f . g) xs == (fmap f . fmap g) xs where types = ([f, g]::[Int -> Int], xs::[Int]) prop_MapMonad f xs = fmap f xs == (xs >>= return . f) where types = (f::Int -> Int, xs::[Int]) prop_Ap f x1 x2 x3 = return f `ap` x1 `ap` x2 `ap` x3 == (liftM3 f x1 x2 x3::[Int]) where types = (f::Int -> Int -> Int -> Int)
prop_MapComposeとprop_MapMonadは第3回で紹介したFunctorクラスが満たすべき法則,prop_Apは前回紹介したap関数の性質についてテストしています。いずれもリストに対しては満たされるため,テストは成功します。
*QuickSort Text.Show.Functions> quickCheck prop_MapCompose +++ OK, passed 100 tests. *QuickSort Text.Show.Functions> quickCheck prop_MapMonad +++ OK, passed 100 tests. *QuickSort Text.Show.Functions> quickCheck prop_Ap +++ OK, passed 100 tests.
また他の値と同様に,==>演算子などを使って,生成する関数を絞りこむこともできます。
prop_AddTwice f x = 0 < x && x <= maxBound ==> f x > x && f x <= maxBound ==> x < f (f x) && f (f x) <= maxBound ==> f (f x) > 0 where types = (f::Int -> Int)
ただし,生成される関数の振る舞いは不定であるため,QuickCheckが値を生成しようと試みる回数をある程度増やさないときちんとテストできないかもしれません。
*QuickSort Text.Show.Functions> quickCheck prop_AddTwice Arguments exhausted after 18 tests. *QuickSort Text.Show.Functions> check (defaultConfig {configMaxFail = 10000}) prop_AddTwice OK, passed 100 tests.
高階関数のテストに使われる関数はどのようにして生成されているのでしょうか?
関数に対するArbitraryクラスのインスタンスを見てみましょう。
instance (Arbitrary a, Arbitrary b) => Arbitrary (a -> b) where arbitrary = promote (`coarbitrary` arbitrary) coarbitrary f gen = arbitrary >>= ((`coarbitrary` gen) . f)
coarbitraryは,ある型aの値とある型bの生成関数を取り,型bの値を生成するメソッドです。
Prelude Test.QuickCheck> :i coarbitrary class Arbitrary a where ... coarbitrary :: a -> Gen b -> Gen b -- Defined in Test.QuickCheck
(`coarbitrary` arbitrary)はセクションであるため,この部分は型aの値を取り型bの値を生成する関数になっています。
Prelude Test.QuickCheck> :t (`coarbitrary` arbitrary) (`coarbitrary` arbitrary) :: (Arbitrary a, Arbitrary b) => a -> Gen b
こうしてできた関数にpromote関数を適用することで,関数の生成関数にしているのです。
Prelude Test.QuickCheck> :i promote promote :: (a -> Gen b) -> Gen (a -> b) -- Defined in Test.QuickCheck
現行バージョンである1.1.0.0の定義では,arbitraryメソッドとcoarbitraryメソッドが同じクラスに属するため,少しわかりにくくなっています。参考までに,以下にQuickCheckの原論文での定義を載せておきます。現在開発中のバージョン2.0も同じ定義を採用しています。
class CoArbitrary a where coarbitrary :: a -> Gen c -> Gen c instance (CoArbitrary a, Arbitrary b) => Arbitrary (a -> b) where arbitrary = promote (`coarbitrary` arbitrary)
coarbitraryとは何でしょうか? 「coarbitraryメソッドが第1引数として型変数aの値を取って最終的に型変数b(またはc)の値を返す」という関係と「arbitraryクラスのインスタンスであるa -> bが型変数aの値を取って型変数bの値を返す」という関係は同じものです。つまり,「ある部分から見て*と共通の関係を持つもの」を「co*」と呼ぶのです。関係が逆になる場合には「contra*」と呼びます。この関係を覚えておけば,coやcontraといった接頭辞が付く名前を見たときに戸惑わずに済むでしょう。
では,関数の生成に必要なcoarbitraryメソッドの定義方法を説明しましょう。coarbitraryメソッドは,variant関数を使って以下のように定義します。ここでインスタンスを定義しているLicenseとTreeは,前回定義したものと同じです。実際に試す場合には,前回のソースコードにcoarbitraryメソッドの定義を書き足すとよいでしょう。
ただし,前回のTree型に対するarbitraryメソッドの定義だと,かなり大きな木が作成される可能性があります。GHCiのプロンプト上でスタック・オーバーフローが発生するかもしれません。こうした問題を防ぐため,小さめの木を生成するようにarbitraryメソッドの定義をsizedTreeに変えます。また,ソースコードにText.Show.Functionsモジュールのインポートを記述しておけば,GHCi上で使用するモジュールを指定する手間を省けます。
Prelude Test.QuickCheck> :t coarbitrary coarbitrary :: (Arbitrary a) => a -> Gen b -> Gen b Prelude Test.QuickCheck> :t variant variant :: Int -> Gen a -> Gen a
import Text.Show.Functions ~ 略 ~ instance Arbitrary License where ~ 略 ~ coarbitrary (GPL) = variant 0 coarbitrary (LGPL) = variant 1 coarbitrary (BSD) = variant 2 coarbitrary (PublicDomain) = variant 3 coarbitrary (OtherLicense) = variant 4 instance Arbitrary a => Arbitrary (Tree a) where arbitrary = sizedTree coarbitrary (Leaf l) = variant 0 . coarbitrary l coarbitrary (Branch t1 t2) = variant 1 . coarbitrary t1 . coarbitrary t2
variantに渡す数値は,最終的に関数が生成する値を調整するものです。引数の値ごとの振る舞いを区別できるものであればどんな数値でもかまいません。ただし,負の数は渡さないようにしてください。
coarbitraryメソッドの定義からどのように「関数の生成関数」が作られているかを見てみましょう。
prop_ComposeAssoc f g h x = ((f . g) . h) x == (f . (g . h)) x where types = [f, g, h] :: [Tree License -> Tree License]
prop_ComposeAssocは,関数合成に結合法則(associative law)が成り立つかどうかを調べるものです。coarbitraryメソッドを使用して作られた関数が,副作用のない純粋な関数としてふるまうなら,このテストは成功するはずです。
*QuickSort> quickCheck prop_ComposeAssoc OK, passed 100 tests.
このように,coarbitraryメソッドを定義することで,TreeやLicenseを対象とする関数を生成できるようになることを確認できました。