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を思い出すと良いかもしれない。