2013年4月12日金曜日

QuickCheck を使う

Real World Haskell の Chapter 11 に QuickCheck の使い方が載っていて、これは素晴らしいと思ったのだけれど、一部記載が古いバージョン (QuickCheck 1) に関するものであり現行バージョン (QuickCheck 2) でそのまま使えない箇所があったため、補完的な意味で書いてみる。
動作環境は以下のとおり。

  • ghc-7.4.2
  • QuickCheck-2.4.2

1. 概要

QuickCheck はユニットテストを行うための仕組みで、テストにランダムな値を生成して与えることができる。具体的な値を決めてテストを実施するわけではないので、テスト通過可否はその性質によって判断する。
以下はクイックソートの例。クイックソート本体 (qsort) の他に、qsort の動作を確認するための関数 prop_sorted を記述している。これは qsort 後のリストが昇順にソートされているかを確認する。
-- file: QuickSort.hs
import Data.List
import Test.QuickCheck

qsort :: Ord a => [a] -> [a]
qsort []     = []
qsort (x:xs) = qsort lhs ++ [x] ++ qsort rhs
    where lhs = filter  (< x) xs
          rhs = filter (>= x) xs

prop_sorted :: Ord a => [a] -> Bool
prop_sorted xs = sorted $ qsort xs
    where sorted (x:xs@(x':_)) = x <= x' && sorted xs
          sorted _             = True
GHCi 上でテスト実施。prop_sorted に 100 通りの値を与えて qsort の動作を確認している。
ghci> :l QuickSort.hs
[1 of 1] Compiling Main             ( qsort.hs, interpreted )
Ok, modules loaded: Main.
ghci> quickCheck (prop_sorted :: [Integer] -> Bool)
+++ OK, passed 100 tests.
具体的には、こんな値を与えている (verboseCheck で確認可能)。
ghci> verboseCheck (prop_sorted :: [Integer] -> Bool)
Passed:  
[]
Passed: 
[]
Passed:  
[0,-2]
Passed:  
[0,3,0]
Passed:  
[-3,-2,-3,4]
--- snip ---
Passed:   
[-29,68,-24,-85,-7,-76,27,34,65,-31,-62,-27,65,-53,76,-82,24,97,-43,-70,-67,58,
56,-95,-48,18,26,-91,97,-56,33,10,-57,29,-61,-60,-23,-87,77,-9,-30,21,-89,-32,
-42,-73,21,60,65,-9,-76]
Passed:   
[97,-9,-27,42,-30,49,-97,46,-48,11,-87,-47,-36,45,-55,-25,2,-58,-43,82,3,81,97,
-28,-84,75,-13,42,-50,0,7,54,46,-13,4,73,-35,-32,-77,-37,-42,81,-3]
Passed:   
[-78,85,3,18,-67,94,99,-17,-66,94,-1,-83,-96,-68,-31,72,-79,81,47,24,-78,20,-87,
-56,38,65,-69,40,-54,-53,42,96,15,62,55,-10,-89,-83,67,36,-85,-97,71,-28,-3,77,
84,-7,57,-55,11,84,-85,-89]
+++ OK, passed 100 tests.
ランダムテストの良いところは、人間が思いもよらない値をテストに与えることができること、具体的な値を定義する手間が省けること。そもそもコードとテストを書く人間が同じ場合、テストパターンとして思い浮かぶ時点で、コードにもその考慮がされていることが多いんじゃないでしょうか。

2. 値の生成

値の生成は System.Random で乱数を発生させて、それに基づき Arbitrary クラスで定義された arbitrary 関数で当該型の値を生成するという流れになる。なお、Gen モジュールでランダムな値を生成するための便利な関数が用意されているため、arbitrary 関数で直接 System.Random の乱数を扱う必要は多分ほとんどない。以下がキーとなる (と RWH に書かれていた) 関数。
-- module: Test.QuickCheck.Gen
elements :: [a] -> Gen a
choose :: Random a => (a, a) -> Gen a
oneof :: [Gen a] -> Gen a
なお、Gen は state-passing monad ということだがモナドはまだよく理解できていない。とにかく、実際に GHCi 上で値を取り出して挙動を確認するためには sample 関数を使用する。
-- module: Test.QuickCheck.Gen
sample :: Show a => Gen a -> IO ()
GHCi 上でテスト。
ghci> :m +Test.QuickCheck
ghci> sample $ elements [0,1,2,3,5,8]
8
3
1
8
0
5
5
2
3
2
8
ghci> sample $ choose ('a','z')
'h'
'f'
'g'
'q'
'c'
'i'
'a'
'a'
'a'
'r'
'g'
ghci> sample $ oneof [choose (0,10), choose (100,110), choose (200,210)]
207
0
9
207
105
8
106
202
8
202
110
これらを使用して、必要な型の乱数値を生成する。例えば、あらかじめ実装されている Char は以下のようになっている。
-- module: Test.QuickCheck.Arbitrary
instance Arbitrary Char where
  arbitrary = chr `fmap` oneof [choose (0,127), choose (0,255)]
これを使用して独自の型の乱数値を生成する例。良い例が思い浮かばなかったので Real World Haskell の例そのまま。
-- Doc.hs
import Test.QuickCheck  
import Control.Monad (liftM, liftM2)  
  
data Doc = Empty  
         | Char Char  
         | Text String  
         | Line  
         | Concat Doc Doc  
         | Union Doc Doc  
         deriving (Show)  
  
instance Arbitrary Doc where  
    arbitrary =  
        oneof [ return Empty  
              , liftM Char arbitrary  
              , liftM Text arbitrary  
              , return Line  
              , liftM2 Concat arbitrary arbitrary  
              , liftM2 Union arbitrary arbitrary ]  
arbitrary は型推論され、関数に必要な型の乱数値を与えている。Text には String を、Concat には再帰して Doc の値を与える。
ghci> :t arbitrary
arbitrary :: (Arbitrary a) => Gen a
ghci> :m +Control.Monad
ghci> :t liftM Text
liftM Text :: (Monad m) => m String -> m Doc
ghci> :t liftM2 Concat
liftM2 Concat :: (Monad m) => m Doc -> m Doc -> m Doc
また、arbitrary 関数中の oneof .. の部分は以下のように書くこともできるが、oneof ... のほうがすっきりしてわかりやすい。
instance Arbitrary Doc where
    arbitrary = do
        n <- choose (1, 6) :: Gen Int
        case n of
             1 -> return Empty

             2 -> do x <- arbitrary
                     return (Char x)
--- snip ---
sample 関数で生成を確認する。
ghci> :l Doc.hs
[1 of 1] Compiling Main             ( Doc.hs, interpreted )
Ok, modules loaded: Main.
ghci> sample (arbitrary :: Gen Doc)  
Char 'r'  
Line  
Union (Text "Ce") (Char '\194')  
Text "\179lj(\169K"  
Text "\233y\189d"  
Union (Union (Char 'K') (Concat (Concat Empty (Char 'c')) Empty)) (Char 'A')  
Text "\213V\236s\ESC\167\NUL\224\ENQ4u"  
Union (Union Empty (Char '\219')) (Char '\138')  
Line  
Union Line (Concat Empty (Concat (Char 'w') Line))  
Char '}'  

3. テストをまとめて実行する

テストの作り方は上に示したが、テストを複数作ってそれをひとつずつ GHCi で実行していくわけにはもちろんいかないので、まとめる。関数の中にひとつずつ quickCheck prop_hoge と記述していっても良いが、全部のテストをまとめて行うための仕組みも用意されている。{-# LANGUAGE TemplateHaskell #-} したうえで $quickCheckAll を実行するだけ。
-- TestSuite.hs
{-# LANGUAGE TemplateHaskell #-}

import Test.QuickCheck
import Test.QuickCheck.All

prop_plus1 :: Integer -> Bool
prop_plus1 x = x + 1 > x

prop_abs :: Integer -> Bool
prop_abs x = abs x >= 0

prop_square :: Integer -> Bool
prop_square x = x ^ 2 > x

main = $quickCheckAll
これで全てのテストが実行される。prop_square は失敗するテストの例。
$ runghc TestSuite.hs 
=== prop_plus1 on TestSuite.hs:6 ===
+++ OK, passed 100 tests.
=== prop_abs on TestSuite.hs:9 ===
+++ OK, passed 100 tests.
=== prop_square on TestSuite.hs:12 ===
*** Failed! Falsifiable (after 1 test):  
0
False
一番大事なことは、テストを書くことをめんどくさがらないことですかね。

参考

Real World Haskell Chapter 11. Testing and quality assurance: http://book.realworldhaskell.org/read/testing-and-quality-assurance.html
Introduction to QuickCheck2: http://www.haskell.org/haskellwiki/Introduction_to_QuickCheck2

0 件のコメント:

コメントを投稿