Haskellでの実例ベーステストとプロパティベーステストの実装

ここ最近話題になることが多いプロパティベーステストはHaskellのQuickCheckを源流としています。そこで今回はHaskellでの実装を通して、実例ベーステストとプロパティベーステストのコードを例示します。

テスト対象

今回はテスト対象として一般的なFizzBuzzを使います。FizzBuzzは15で割り切れるならFizzBuzz、それ以外で、F3で割り切れるならばFizz、5で割り切れるならBuzzを表示するというプログラム課題です。
テスト対象のコードを以下に示します。

fizzbuzz :: Int -> String
fizzbuzz n  
    | n `mod` 15 == 0  = "FizzBuzz"
    | n `mod` 3  == 0  = "Fizz"
    | n `mod` 5  == 0  = "Buzz"
    | otherwise        = show n

FizzBuzzの実例ベーステスト

Haskellの実例ベーステストのテストフレームワークは、HUnit、HSpec、doctestと様々ありますが、今回はxUnitファミリーのHUnitを使います。
実装は次のようになります。

import Test.HUnit

~

tests :: Test
tests = TestList
    [ "if input mod 3 == 0, print Fizz"      ~: "Fizz" ~=? fizzbuzz 6
    , "if input mod 5 == 0, print Buzz"      ~: "Buzz" ~=? fizzbuzz 25
    , "if input mod 15 == 0, print FizzBuzz" ~: "FizzBuzz" ~=? fizzbuzz 30
    , "if input mod 3 != 0 and input mod 5 != 0, print input" ~: "31" ~=? fizzbuzz 31
    ]

main :: IO ()
main = runTestTT tests

上記では、HUnitの構文を使って、データ駆動テストのように実装しています。実例ベーステストの名の通り、具体的な入力値と期待値のセットの実例を列挙しています。
実例という、膨大な入力パターンの中の点をサンプリングチェックしているだけであり、テストとしては不足があります(上記の例では、(例示のためあえてですが)負値といった異常値や上限下限のテストがないなど)。

実行結果は次のようになります。

Cases: 4  Tried: 4  Errors: 0  Failures: 0

FizzBuzzのプロパティベーステスト

次はQuickCheckを使ったプロパティベーステストについてです。FizzBuzzの一部の仕様に限定してテストするものですが、実装は次のようになります。

import Test.QuickCheck

~

prop_fizzbuzz_FizzBuzz :: Int -> Property
prop_fizzbuzz_FizzBuzz n = n > 0 && n `mod` 15 == 0 ==> fizzbuzz n == "FizzBuzz"

prop_fizzbuzz_Number :: Int -> Property
prop_fizzbuzz_Number n = n > 0 && n `mod` 3 /= 0 && n `mod` 5 /= 0 ==> fizzbuzz n == show n

main :: IO ()
main = do
  quickCheck prop_fizzbuzz_FizzBuzz
  quickCheck prop_fizzbuzz_Number

具体的な入力・期待値の実例のセットをテストで記述せず、入出力の関係性をプロパティとして実装しています。入力はランダム生成され、プロパティ(≒事前条件・事後条件の関係性)を広い範囲でテストします。

実行結果は次のようになります。

*** Gave up! Passed only 7 tests; 1000 discarded tests.
+++ OK, passed 100 tests; 288 discarded.

プロパティベーステストの利点と課題

広い入力パターンの一点だけを確認する実例ベーステストに対し、プロパティベーステストはファジングのようにランダム生成で広い範囲のテストを行います。
ただ以下の課題もあります。

  1. テストオラクル問題を解決しなければならない。以下のような手段で期待値を確保する必要がある。今回の例のようなテスト対象と二重実装になるテストオラクルは有効でない。
  2. テストカバレッジ(仕様カバレッジやコードカバレッジなど)を保証できない。モデル検査やコンコリックテストといった、網羅率を保証する仕組みがないと、プロパティを十分に網羅できたかわからない
  3. 高いリスクに対応できているかわからない(例えば今回の例なら負値や上下限値、整数以外の入力に対するテストなど)。プロパティベーステストのフレームワークには、失敗するパターンの比重を厚くするといった学習機能を有するものもあるが、最初からリスクに対応するためには、実例ベーステストのように入力を決め打ちする必要がある

上記の課題は完全対応が難しい場合も多いため、通常、プロパティベーステストは実例ベーステストと組み合わせて弱みを補う必要があります。