Haskell: QuickCheckの意外な使い方
QuickCheckは想像以上にパワフルなツールだ。単にテストをしてくれるだけのツールじゃない。
以下の文書を読むと、QuickCheckの意外な使い方が書いてあった。Haskell以外のプログラマーも目を通すことをおすすめする。
この文書で説明されている例は凄い。あるパーサに入力するためのテキスト(ある言語のステートメントの列)を自動生成させるために、QuickCheckを使っている。この例の場合は、まずプログラマがターゲットの言語の文法の仕様を定義しておいて、後はQuickCheckが膨大な組み合わせのステートメントを自動生成してくれている。パーサの動作チェックをするために、膨大なステートメントの組み合わせを考えながら、パーサに入力するテキストを人間が作っていたのではしんどすぎる。だから、網羅的なテストを楽に行うにはQuickCheckのような手法が必要になる。
「QuickCheckを使えば、コーナーケースを突付くようなテストが可能になる」という意味がようやくわかるようになった。
上で挙げた文書に載っていたコードを参考にして、適当なHTML文書を生成させるプログラムを書いてみた。本当に適当なHTML文書を自動生成してくれる。でも一応はHTML文書になっている。QuickCheckは内部で乱数を使っているので、当然実行結果は毎回変わる。
<html> <body> <h1>ukqffmjwmk</h1> <a href="http://yjjmstufhf.com">agdnglsxxy</a> <a href="http://lsjazazhct.com">fcqtktayrx</a> <h2>vqcfwynvgj</h2> <br/> <h2>svqqdmbgec</h2> <p>ttflcachyp</p> <h1>njczmhhaku</h1> <a href="http://bsxgwuzyfp.com">paptaobrnx</a> <a href="http://drncwunaqt.com">wytaoqktin</a> </body> </html>
コードは以下の通り。invalidなHTMLは生成しないことにしたので、上の文書で出てくるVariantクラスは使わなかった。代わりにValidクラスを定義してシンプルにした。また簡単にするために、HTMLはh1、h2、p、a、brから構成されるものとした。一応、aタグのhref属性はURLっぽい文字が使われるようにした。
% vim SimpleHtmlGenerator.hs {-# OPTIONS -fglasgow-exts #-} import System.Random import Control.Monad import Test.QuickCheck --------------------------------------- -- HTML elements data Elem = H1 String | H2 String | P String | A Url String -- href, content | BR Void deriving (Eq) data Url = Url String deriving (Eq) data Void = Void deriving (Eq) instance Show Elem where show(H1 s) = "<h1>" ++ s ++ "</h1>" show(H2 s) = "<h2>" ++ s ++ "</h2>" show(P s) = "<p>" ++ s ++ "</p>" show(A (Url h) c) = "<a href=\"" ++ h ++ "\">" ++ c ++ "</a>" show(BR v) = "<br/>" --------------------------------------- -- class Valid a where valid :: Gen a instance Valid String where valid = oneof [rndStrGen "" 10] instance Valid Url where valid = oneof [urlGen] instance Valid Void where valid = voidGen instance Valid Elem where valid = oneof [ liftM H1 valid , liftM H2 valid , liftM P valid , liftM2 A valid valid , liftM BR valid ] --------------------------------------- -- Generators elementGen :: Gen Elem elementGen = do e <- valid return e urlGen :: Gen Url urlGen = do let scheme = "http://" host <- rndStrGen "" 10 return $ Url (scheme ++ host ++ ".com") rndStrGen :: String -> Int -> Gen String rndStrGen s 0 = return s rndStrGen s n = do let a_z = map (:[]) ['a' .. 'z'] e <- elements a_z let r = s ++ e rndStrGen r (n - 1) wordGen :: Gen String wordGen = do e <- elements["foo", "bar", "baz"] return e voidGen :: Gen Void voidGen = do return (Void) gen :: Gen Elem -> IO String gen g = do rnd <- newStdGen let r = generate 10 rnd g return $ show r --------------------------------------- -- main main :: IO () main = do putStrLn "<html>\n<body>" printElements 10 putStrLn "</body>\n</html>" printElements :: Int -> IO () printElements 0 = return () printElements n = do s <- gen elementGen putStrLn s printElements (n - 1)
網羅的なテストをしたいんだけど、と思ったらQuickCheckを思い出すと良いかもしれない。