HUnit とその自動化

最近テストファースト的なものを試しているので、Haskell の HUnit を使ってちょっと遊んでみた。ところが、実施するテストを選ぶのがめんどい。具体的にはこんな感じ。(HUnit 1.0 User's Guide)

test1 = TestCase (assertEqual "for (foo 3)," (1,2) (foo 3))
test2 = TestCase (do (x,y) <- partA 3
                     assertEqual "for the first result of partA," 5 x
                     b <- partB y
                     assertBool ("(partB " ++ show y ++ ") failed") b)

tests = TestList [TestLabel "test1" test1, TestLabel "test2" test2]

じゃあ Template Haskell でも使ってなんかやってみるかー、という企画。完全に手段が目的になっている。自動化については第16回 Haskellでのテストの自動化を考える(2ページ目) | 日経 xTECH(クロステック)で触れられています。あとで読みます。以下ソースをべたっと貼ってあって長いです。

P2TH

Language.Haskell.Syntax の構文木から Language.Haskell.TH.Syntax への本当に超適当変換器。今回使ったものしか変換できないのでちょっと変な入力を与えるとすぐ落ちるはず。しかもそれぞれの木について詳しく調べてないので間違いがあるかもしれない。もっとちゃんとしたやつがどっかにあるんじゃないかなぁ。

{-# OPTIONS -fth -fglasgow-exts #-}
module AutoTest.P2TH where
    import Language.Haskell.Syntax
    import Language.Haskell.TH
    import Test.HUnit

    p2TH_decl :: HsDecl -> ExpQ
    p2TH_decl (HsPatBind _ (HsPVar (HsIdent i)) (HsUnGuardedRhs exp) _) =
        [| TestLabel i $ TestCase $(p2TH_exp exp) |]
    p2TH_exp :: HsExp -> ExpQ
    p2TH_exp (HsApp x y) = appE (p2TH_exp x) (p2TH_exp y)
    p2TH_exp (HsInfixApp x op y) =
        infixE (Just $ p2TH_exp x) (p2TH_op op) (Just $ p2TH_exp y)
    p2TH_exp (HsVar x)   = varE (p2TH_qname x)
    p2TH_exp (HsLit x)   = litE (p2TH_literal x)
    p2TH_exp (HsCon x)   = conE (p2TH_qname x)
    p2TH_exp (HsParen x) = p2TH_exp x
    p2TH_exp (HsNegApp x) = (appE [|negate|] $ p2TH_exp x)
    p2TH_qname (UnQual (HsIdent i)) = mkName i
    p2TH_qname (UnQual (HsSymbol s)) = mkName s
    p2TH_op (HsQVarOp op) = varE (p2TH_qname op)
    p2TH_literal (HsString s) = StringL s
    p2TH_literal (HsInt x) = integerL x

Tester

checker はソースファイル(target)をパースして指定された正規表現(pattern)にマッチする関数名を抜き出し、ひとつの関数 check の定義を出力する関数。

{-# OPTIONS -fth -fglasgow-exts #-}
module AutoTest.Tester (checker) where
import AutoTest.P2TH
import Language.Haskell.Parser
import Language.Haskell.Syntax
import Language.Haskell.TH
import Text.Regex.Posix
import Test.HUnit
import Calc

checker target pattern =
    do src <- runIO $ readFile target
       case parseModule src of
         ParseFailed loc mes -> error $ show loc ++ mes
         ParseOk (HsModule _ _ _ _ decls) ->
             do let testers = filter (isMatch (=~ pattern)) decls
                let testers' = listE $ map p2TH_decl testers
                check <- valD (varP $ mkName "check")
                         (normalB [|runTestTT (TestList $testers')|]) []
                return [check]

isMatch pat (HsPatBind _ (HsPVar (HsIdent i)) _ _) = pat i
isMatch _ _ = False

Calc

そんでこれがテストされるモジュール Calc。なんかちょっと高級な計算機みたいなものを作ろうとしてたけど途中からそっちのけになった。なので実装はまだありませぬ。一番下にあるのがテスト。テスト関数はもちろん別のファイルに分離してもよい。

module Calc where
import Test.HUnit

data Exp = N Rational
	 | V String
	 | Root Exp
	 | Sin  Exp | Cos  Exp | Tan  Exp
	 | Differential Exp | Integral Exp
	 | Plus Exp Exp
	 | Sub  Exp Exp
	 | Mult Exp Exp
	 | Div  Exp Exp
	   deriving (Eq, Show)
isN (N _) = True
isN _     = False
isV (V _) = True
isV _     = False

bindup :: Exp -> Exp
bindup = undefined

eval :: Exp -> Exp
eval x = x


test_plus = assertEqual "plus 1 3," (N 3) (eval $ Plus (N 1) (N 2))
test_mul  = assertEqual "mult 2 3," (N 6) (eval $ Mult (N 2) (N 3))
test_sub  = assertEqual "sub 3 5," (N (-2)) (eval $ Sub (N 3) (N 5))
test_div  = assertEqual "div 3 9," (N (1 / 3)) (eval $ Div (N 3) (N 9))
test_plus_mul = assertEqual "3+2*9" (N (3+2*9))
		(eval $ Plus (N 3) (Mult (N 2) (N 9)))

Test_Calc

Calc をテストするモジュール Test_Calc。checker を使うだけ。

{-# OPTIONS -fth -fglasgow-exts #-}
module Test_Calc where
import Test.HUnit
import AutoTest.Tester
import Calc

$( checker "Calc.hs" "test_")

このモジュールのコンパイルが終わると checker 関数が生成されるので、check を自分で(!)呼んでテストする。

*Test_Calc> check
### Failure in: 0:test_plus
plus 1 3,
expected: N (3%1)
 but got: Plus (N (1%1)) (N (2%1))
### Failure in: 1:test_mul
mult 2 3,
expected: N (6%1)
 but got: Mult (N (2%1)) (N (3%1))
### Failure in: 2:test_sub
sub 3 5,
expected: N ((-2)%1)
 but got: Sub (N (3%1)) (N (5%1))
### Failure in: 3:test_div
div 3 9,
expected: N (1%3)
 but got: Div (N (3%1)) (N (9%1))
### Failure in: 4:test_plus_mul
3+2*9
expected: N (45%1)
 but got: Plus (N (3%1)) (Mult (N (2%1)) (N (9%1)))
Cases: 5  Tried: 5  Errors: 0  Failures: 5
Counts {cases = 5, tried = 5, errors = 0, failures = 5}
*Test_Calc> 

テストにことごとく失敗しているけど気にしない。ほんとはインタプリタでロードするだけで自動でテストが走るようにしたい。もうひとつファイルを増やすとできるんだけど、なんか無駄にファイルが増えていやだ。

感想

Template Haskell ではコンパイル時に実行する関数は基本的にインポート先で定義されていないといけないし、Haskell では2つ以上のモジュールがお互いをインポートしてはいけないので、テストに必要なファイルの数が増える。あんまり考えずにすぐに実装を始めたので、もっといい方法があるような気がする。
まぁ楽しかったのでよい。