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