テスト範囲及びテスト数の変更

 QuickCheckは,ある型が取りうるすべての値を網羅するツールではありません。ランダムに作成した100個の値についてテストを行います。

*QuickSort Test.QuickCheck> quickCheck prop_Quicksort
OK, passed 100 tests.

 値をランダムに作成しているのには理由があります。テストは現実的な時間内に終わらなければならないからです。Integer型のような多倍長整数(Arbitrary-precision integers)でなくInt型であっても,32ビットや64ビットの範囲を考えれば膨大な量になります。また,Haskellでは無限の大きさを持つ値が存在しえます。こうした値を扱おうとすると,テストが終了することは決してないでしょう。必ず限られた値でテストしなければなりません。

 ただし,値を限定する以上,どんな値をテストに使うかに気を配る必要があります。使う値によっては,テストがほとんど意味をなさない可能性があります。

 例として,「任意の整数は30よりも小さい」という式「\x -> x < 30」に対してテストを行ってみましょう。xがIntであれIntegerであれ,標準で定義されている通常の数値型であれば30より大きい値が存在します(参考リンク)。したがって,必ず失敗するという結果を期待するでしょう。

 しかし,実際にテストを実行してみると,成功したり失敗したり,その時々で違う結果になります。

Test.QuickCheck> quickCheck $ \x -> x < 30
Falsifiable, after 87 tests:
44
Test.QuickCheck> quickCheck $ \x -> x < 30
OK, passed 100 tests.
~ 略 ~

 QuickCheckは値をランダムに生成するため,生成した値の中に30より大きい数値が含まれるかどうかは,その時々によって異なるのです。

 また,比較対象の数値の大きさによっても,失敗の頻度は変わります。20であればほとんど失敗しますし,100であれば必ず成功します。こうした数値による違いが見られるのは,QuickCheckがデフォルトで生成する数値の範囲をわざと狭く設定しているからです。この範囲は狭いまま固定されているわけではなく,値を生成するにつれてだんだん広くなっていくように実装されています(実例は後で述べます)。

 なぜこのような実装になっているのでしょうか? 一つ目の理由は,テストが失敗したときの反例をできるだけわかりやすくするためです。同じ失敗でも,大きな値よりも小さな値のほうがバグを見つけやすくなります(参考リンク)。

 二つ目の理由は,テスト自体に存在する問題を発見するためです。上の例ではテストそのものには記述されていない恣意的な仮定が存在します。先ほど「標準で定義されている通常の数値型であれば,30より大きい値が存在するはず」と仮定しました。この仮定は30のような小さな数値に対しては正しいですが,任意の数値xに対していつも正しいとは限りません。例えば,Int型が使える数値の範囲には限界があります。また,標準で定義されていないユーザー定義の型の場合,30のような小さな数値に対しても仮定が正しくない可能性があります。

 こうした不正な数値に対する検査を静的に行うことができれば,何も問題はないでしょう。しかし,標準Haskellの仕様ではそのようなことはできません。整数や浮動少数といった数値リテラルの定義は,型自身の定義からではなく,型クラスのメソッドによって与えられるからです(参考リンク)。この結果,ある数値が型に含まれているかどうかを静的に調べることはできないようになっています。

invalidInt :: Int
invalidInt = 21474836455434543535434747788957367367367345

*QuickSort> :r
[1 of 1] Compiling QuickSort        ( QuickSort.hs, interpreted )
Ok, modules loaded: QuickSort.
*QuickSort> :t invalidInt
invalidInt :: Int

 静的に調べられない以上,ある値がその型に存在するかどうかを保証するのはユーザーの仕事になります。こうした仮定は正しいことが完全に保証されている場合には良いのですが,そうでない場合にはバグの温床になります。こうした仮定を取り除くために,静的にも動的にもテストされない恣意的な仮定を持つテストは失敗する可能性があるように生成する値を設定しているのです。

 使用する値がテストされるべきものを十分に反映していないと感じたら,普通はテスト数を増やすでしょう。しかし,テスト数を増やしても,使う値が限定されている限り,テスト数を増やす効果がないかもしれません。このような事態を避けるために,QuickCheckでは実行したテスト数が増えるにつれて,値を生成する範囲が徐々に広がるように実装されているのです。

 テストに使う値を明示的に広げることも可能です。使う値を広げるには,ランダムの作成する値の数を増やす,生成する数値の範囲を広げる,何らかの演算を施して数値を大きくする,の三つの方法があります。

 例を見ましょう。テストに使用する値を単純にカスタマイズするには,forAll(「すべての」という意味)関数が使えます。

Prelude Test.QuickCheck> :t forAll
forAll :: (Testable b, Show a) => Gen a -> (a -> b) -> Property

 これを使ってarbitraryメソッドの生成する数値の範囲を4倍に広げれば,100を使って比較してもほとんど失敗するようになります。

Prelude Test.QuickCheck> quickCheck $ forAll (sized $ \n -> resize (4*n) arbitrary) $ \x -> x < 100
Falsifiable, after 91 tests:
184
Prelude Test.QuickCheck> quickCheck $ forAll (sized $ \n -> resize (4*n) arbitrary) $ \x -> x < 100
Falsifiable, after 73 tests:
108
Prelude Test.QuickCheck> quickCheck $ forAll (sized $ \n -> resize (4*n) arbitrary) $ \x -> x < 100
Falsifiable, after 72 tests:
130
~ 略 ~
Prelude Test.QuickCheck> quickCheck $ forAll (sized $ \n -> resize (4*n) arbitrary) $ \x -> x < 100
OK, passed 100 tests.

 この例ではsized関数とresize関数を使い,生成される数値の範囲を4倍に広げています。

Prelude Test.QuickCheck> :t sized
sized :: (Int -> Gen a) -> Gen a
Prelude Test.QuickCheck> :t resize
resize :: Int -> Gen a -> Gen a

 次に,テストの数を増やす方法を説明しましょう。

 Config型を追加の引数として取るcheck関数を使うことでQuickCheckの振る舞いをカスタマイズできます。

Prelude Test.QuickCheck> :t quickCheck
quickCheck :: (Testable a) => a -> IO ()
Prelude Test.QuickCheck> :t check
check :: (Testable a) => Config -> a -> IO ()
Prelude Test.QuickCheck> :i Config
data Config
  = Config {configMaxTest :: Int,
            configMaxFail :: Int,
            configSize :: Int -> Int,
            configEvery :: Int -> [String] -> String}
        -- Defined in Test.QuickCheck

 今回,関心があるのは,Config型のうちテスト数を示すconfigMaxTestフィールドだけです。そこで「quickCheck関数と同等になるようなConfig型のデフォルトの値」を定義しているdefaultConfigを使ってテスト数を増やすことにしましょう。

Prelude Test.QuickCheck> check (defaultConfig {configMaxTest = 1000}) $ \x ->  x < 100
Falsifiable, after 242 tests:
104
Prelude Test.QuickCheck> check (defaultConfig {configMaxTest = 1000}) $ \x ->  x < 100
Falsifiable, after 282 tests:
112

 失敗するまでに行われたテストの数から,テスト数が増えていることを確認できますね。

値によらないテスト

 これまでは「任意の数は*よりも小さい」という条件を使ってテストしてきました。ここで「任意の数は*よりも小さくはない」と否定したり,「任意の数は*よりも大きい」というように比較の向きを逆にするとどうなるでしょうか? これまでのテスト結果が引っくり返るのではなく,すべてのテストが失敗することになります。

Prelude Test.QuickCheck> quickCheck $ \x ->  not $ x < 10
Falsifiable, after 0 tests:
2
Prelude Test.QuickCheck> quickCheck $ \x ->  not $ x < 20
Falsifiable, after 0 tests:
0
Prelude Test.QuickCheck> quickCheck $ \x ->  not $ x < 30
Falsifiable, after 0 tests:
-1
Prelude Test.QuickCheck> quickCheck $ \x ->  not $ x < 100
Falsifiable, after 0 tests:
0
Prelude Test.QuickCheck> quickCheck $ forAll (sized $ \n -> resize (4*n) arbitrary) $ \x -> not $ x < 100
Falsifiable, after 0 tests:
2
Prelude Test.QuickCheck> check (defaultConfig {configMaxTest = 1000}) $ \x -> not $ x < 100
Falsifiable, after 0 tests:
2
Prelude Test.QuickCheck> quickCheck $ \x ->  x > 10
Falsifiable, after 0 tests:
0
Prelude Test.QuickCheck> quickCheck $ \x ->  x > 100
Falsifiable, after 0 tests:
-1
Prelude Test.QuickCheck> quickCheck $ forAll (sized $ \n -> resize (4*n) arbitrary) $ \x -> x > 100
Falsifiable, after 0 tests:
0
Prelude Test.QuickCheck> check (defaultConfig {configMaxTest = 1000}) $ \x -> x > 100
Falsifiable, after 0 tests:
1

 こうした結果自体は,数値型の定義から期待されるものです。しかし,テストの意図はほとんど変わらないにもかかわらず,テスト結果が安定するかどうかが異なってしまいました。このような「生成される値によってテスト結果が変わる」という振る舞いは,少し気持ち悪く思えます。生成される値をカスタマイズする以外に,テストを改善する方法はないでしょうか?

 テストの値への依存をなくす第1歩は,なぜテストが生成される値に依存するかを考えることです。ここまで挙げたテストでは,どれも「任意の数は*よりも~」という条件の数*を20や100といった具体的な値に設定していました。これにより,その数値以上(または以下)の数値をテストが生成しなければ正しい結果が得られないことになってしまっていたのです。

 この問題を解決するには,具体的な値を使うのをやめるしかありません。数*を変数に置き換えて,テストを書き換えてみましょう。

Prelude Test.QuickCheck> quickCheck $ \x -> x+1 < x
Falsifiable, after 0 tests:
2
Prelude Test.QuickCheck> quickCheck $ \x y -> x+y < x
Falsifiable, after 0 tests:
-1
2

 新しいテストでは,どのような場合に「任意の数は*よりも小さい」という条件が満たされなくなるかが式として陽に記述されています。Integer型では,もはや生成される値によってテスト結果が揺らぐことはありません。

 ただしInt型の場合には,このテストはまだ完璧ではありません。何が欠けているのでしょうか?

 第4回で説明したように,Int型には最小値や最大値が存在することが標準Haskellで定められています。つまり,最大値より大きな値や最小値より小さな値は存在できないのです。最大値に対して正の数を足すと,桁あふれ(overflow)が起こって正しい計算が行われません。

Prelude> (maxBound::Int) + 1
-2147483648
Prelude> let x = (maxBound::Int) in x + 1 < x
True

 これは,Int型では数*に対してある種の制約を課す必要があることを意味します。

 QuickCheckでは,値に対する制約を書くための演算子が用意されています。

Prelude Test.QuickCheck> :i (==>)
(==>) :: (Testable a) => Bool -> a -> Property
        -- Defined in Test.QuickCheck
infixr 0 ==>

 ==>演算子は実際にテストを行う前に第1引数の条件式と比較し,偽であれば値の生成のやり直しを要求します。この演算子を使うことで,テストで使用する値に対して「**ならば(==>)」という前提条件を書けるようになります。例えば,数*を自然数にしたい場合や整列済みのリストのみに対してテストを行いたい場合には,==>演算子を使って以下のように書くことができます。

*QuickSort> quickCheck $ \x -> x >= 0 ==> x < 1
Falsifiable, after 4 tests:
3
*QuickSort> quickCheck $ \xs -> isSorted xs ==> prop_Quicksort xs
OK, passed 100 tests.

 ただ,前提条件によっては生成される値がいつまでも条件を満たさないことがあります。こうした場合にテストが終了しなくなるのを防ぐため,QuickCheckではあらかじめ失敗の上限を決めています。defaultConfigのconfigMaxFailに定義されている1000という値です。

Prelude Test.QuickCheck> configMaxFail defaultConfig
1000

 この上限を超えて値の生成が失敗した場合,QuickCheckはテストを打ち切り,以下のようなエラー・メッセージを出力します。

Prelude Test.QuickCheck> quickCheck $ \x -> x > 100 ==> x+1 < x
Arguments exhausted after 0 tests.
こうしたエラーが生じる場合,==>演算子の代わりに先に紹介したforAll関数を使うとよいでしょう。Gen型はFunctorクラスやMonadクラスのインスタンスとしても定義されているため,fmapやliftMなどを使用して生成されるすべての値に対して演算を行うことが可能です。ただし,この演算の結果得られる値は,生成される元の値に依存することになります。また,テストの意味合いが「任意の値が**ならば」から「**であるすべての値」というように変わってしまいます。いつでもこの方法が使えるわけではないことに注意してください。

Prelude Test.QuickCheck> :i Gen
newtype Gen a
  = Test.QuickCheck.Gen (Int -> System.Random.StdGen -> a)
        -- Defined in Test.QuickCheck
instance Monad Gen -- Defined in Test.QuickCheck
instance Functor Gen -- Defined in Test.QuickCheck

Prelude Control.Monad Data.List> quickCheck $ forAll (liftM abs arbitrary) $ \x -> x < 1 
Falsifiable, after 1 tests:
1
Prelude Control.Monad Data.List> quickCheck $ forAll (liftM ((+100) . abs) arbitrary) $ \x -> x > 100
Falsifiable, after 0 tests:
100
Prelude Control.Monad Data.List> quickCheck $ forAll (liftM sort arbitrary) $ isSorted
OK, passed 100 tests.
Prelude Control.Monad Data.List> quickCheck $ forAll (liftM sort arbitrary) $ prop_Quicksort
OK, passed 100 tests.

 この例で使用しているabsはNumクラスのメソッドで,数の絶対値(absolute value)を返します。

Prelude> :i Num
class (Eq a, Show a) => Num a where
  (+) :: a -> a -> a
  (*) :: a -> a -> a
  (-) :: a -> a -> a
  negate :: a -> a
  abs :: a -> a
  signum :: a -> a
  fromInteger :: Integer -> a
        -- Defined in GHC.Num
instance Num Double -- Defined in GHC.Float
instance Num Float -- Defined in GHC.Float
instance Num Int -- Defined in GHC.Num
instance Num Integer -- Defined in GHC.Num
Prelude> abs 10
10
Prelude> abs (-10)
10

 同じく,sort関数もData.Listモジュールで定義されている標準のものです(参考リンク)。

Prelude Data.List> :t sort
sort :: (Ord a) => [a] -> [a]

 実装は処理系に依存します。性能上の理由から,現在のGHCではクイックソートではなくマージソート(merge sort)として実装されているようです(参考リンク)。

 では,==>演算子を使って,値が最小値と最大値の間に収まるという制約をテストに追加してみましょう。

Prelude Test.QuickCheck> quickCheck $ \x -> let types = (x::Int) in minBound <= x && x <= maxBound ==> x+1 < x
Falsifiable, after 0 tests:
-1
Prelude Test.QuickCheck> quickCheck $ \x -> let types = (x::Int) in minBound <= x && x <= maxBound && (x+1) <= maxBound ==> x+1 < x
Falsifiable, after 0 tests:
0
Prelude Test.QuickCheck> quickCheck $ \x y -> let types = (x::Int, y::Int) in minBound <= x && x <= maxBound ==> x+y < x
Falsifiable, after 0 tests:
1
1
Prelude Test.QuickCheck> quickCheck $ \x y -> let types = (x::Int, y::Int) in minBound <= x && minBound <= (x+y) && (x+y) <= maxBound ==> x+y < x
Falsifiable, after 0 tests:
-2
1

 第11回で説明したように,<=演算子は非結合であるため,xが最小値と最大値の間に収まるという条件を「minBound <= x <= maxBound」というようにまとめて書き下すことはできません。

Prelude Test.QuickCheck> quickCheck $ \x -> let types = (x::Int) in minBound <= x <= maxBound ==> x+1 < x

<interactive>:1:43:
    precedence parsing error
        cannot mix `(<=)' [infix 4] and `(<=)' [infix 4] in the same infix expression

 さて,これでようやくテストの生成する値への依存をなくすことができました。これらのテストでは「任意の数*+1は*よりも小さくない」と否定したり,「任意の数*+1は*よりも大きい」と比較の向きを逆にすると,テストの意味が引っくり返るため期待通り正反対の結果が返ってきます。

Prelude Test.QuickCheck> quickCheck $ \x -> let types = (x::Integer) in not $ x+1 < x
OK, passed 100 tests.
Prelude Test.QuickCheck> quickCheck $ \x -> let types = (x::Integer) in x+1 > x
OK, passed 100 tests.
Prelude Test.QuickCheck> quickCheck $ \x -> let types = (x::Int) in minBound <= x && x <= maxBound ==> not $ x+1 < x
OK, passed 100 tests.
Prelude Test.QuickCheck> quickCheck $ \x -> let types = (x::Int) in minBound <= x && x <= maxBound ==> x+1 > x
OK, passed 100 tests.
Prelude Test.QuickCheck> quickCheck $ \x -> let types = (x::Int) in minBound <= x && x <= maxBound && (x+1) <= maxBound ==> not $ x+1 < x
OK, passed 100 tests.
Prelude Test.QuickCheck> quickCheck $ \x -> let types = (x::Int) in minBound <= x && x <= maxBound && (x+1) <= maxBound ==> x+1 > x
OK, passed 100 tests.

 このように,テストをより正確に行うには,そのテストが正しくあるべき暗黙の前提を頭の中から実際のコードへと書き写すことが大切です。ある数値xよりも大きな値が存在するために失敗すべきテストなら,そのための前提であるx+1,x+2...が存在するということを実際のコードに写したほうが,より正しくテストを行えます。生成される値によって成功したり失敗したりするようなテストがある場合には,まずそのテストの中に暗黙の前提が隠れていないかどうか検討しましょう。