エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

HaskellのUnitTest、HUnitについて学ぶ

恥ずかしながら。ごく最近までHaskellにUnitTestのライブラリがあるのを知りませんでした。恥ずかしい。
調べてみたところ、思ったよりも簡単にUnitTestを書けることがわかったので、ちょっとまとめてみました。
間違いありましたらご指摘いただけるとたいへんありがたいです。


詳細についてはリファレンスをあたってみてください。

HUnitのインポート

HUnitを使うにはTest.HUnitモジュールをインポートします。

import Test.HUnit


GHCiで利用するばあい。

$ ghci
Prelude> :m Test.HUnit
Prelude Test.HUnit>

アサーション

アサーションは基本的に、常に失敗、Falseのとき失敗、不一致のとき失敗、の3種類です。それぞれ通常の関数での記法と演算子での記法があります。

assertFailure message 常に失敗する。messageを表示する
assertBool message cond condがFalseのばあい失敗する。失敗した場合messageを表示する
assertEqual message expected actual expectedとactualが等しくないばあい失敗する。失敗した場合messageを表示する
assertString message messageが空文字列("")でないばあい失敗する。失敗した場合messageを表示する
expected @=? actual expectedとactualが等しくないばあい失敗する。assertEqual "" expected actualと同じ
actual @?= expected expectedとactualが等しくないばあい失敗する。assertEqual "" expected actualと同じ
cond @? message condがFalseのばあい失敗する。assertBool "" condと同じ


assertFailureassertStringはどちらも単に失敗を発生させるアサーションですが、assertStringは引数の文字列が空のばあい失敗を発生させません。

 Prelude Test.HUnit> assertFailure "hoge"
 *** Exception: HUnitFailure "hoge"
 Prelude Test.HUnit> assertFailure ""
 *** Exception: HUnitFailure ""
 Prelude Test.HUnit> assertString "hoge"
 *** Exception: HUnitFailure "hoge"
 Prelude Test.HUnit> assertString ""
 Prelude Test.HUnit>


ちなみに。アサーションはすぐに評価されるので、上記のようにGHCiで簡単に評価結果を確認することができます。


アサーションの型AssertionIO ()の別名なので、複数のアサーションをdo記法でまとめることができます。

assertHoge = do
    10 @=? sum [1,2,3,4]
    24 @=? product [1,2,3,4]
    "hoge" @=? "HOGE"

テストケース

アサーションからテストケースを生成します。テストケースを生成しただけでは評価はされず、別途テストの実行が必要になります。

TestCaseTestLabelTestListがテストケースのコンストラクタです。アサーションと同じように通常の関数での記法と演算子での記法があります。またアサーション単体、アサーションのリスト、テストケースのリストなどからテストケースを生成するtestというユーティリティな関数があります。

TestCase assertion アサーションからテストケースを生成する
TestLabel label test 既存のテストケースにラベルを追加したテストケースを生成する
TestList tests テストケースのリストから(単一の)テストケースを生成する
expected ~=? actual TestCase $ expected ~=? actualと同じ
actua ~?= expected TestCase $ actua ~?= expectedと同じ
cond ~? message TestCase $ cond @? messageと同じ
label ~: test TestLabel label testと同じ
test testables Testableインスタンスからテストケースを生成する


次のふたつは同じ意味になります。

TestLabel "hoge" $ TestCase $ assertEqual "" 1 2
"hoge" ~: 1 ~=? 2

テストの実行

テストの実行にはrunTestTT関数を使うと簡単です。ちなみに“TT”の意味は“Text-based reporting to the Terminal”だそうです。

runTestTT $ TestLabel "hoge" $ TestCase $ assertEqual "" 1 2


上記のテストをGHCiで実行すると次のような結果が表示されます。

 Prelude Test.HUnit> runTestTT $ TestLabel "hoge" $ TestCase $ assertEqual "" 1 2
 ### Failure in: hoge                      
 expected: 1
  but got: 2
 Cases: 1  Tried: 1  Errors: 0  Failures: 1
 Counts {cases = 1, tried = 1, errors = 0, failures = 1}


runTestTT関数は結果としてIO Counts型の値を返します。Counts型には4つのInt型の値、casestriederrorsfailuresがあって、それぞれテストケースの個数、実施した個数、エラーになった個数、失敗した個数が格納されています。ちなみに「エラー」が具体的に何をあらわしているかは、まだ調べきれてないです。


上のテストは演算子を使って次のように書くことができます。

runTestTT $ "hoge" ~: 1 ~=? 2


コンパイルして実行するばあい。runTestTTの戻り値の型がIO Countsなのでmainの型とあいません。本来ならCountsの値を取り出してレポートを出力するのがよいのでしょうが、手っ取り早くはdo記法でreturn ()を最後に書けばどうにかなります。

main :: IO ()
main = do
    runTestTT $ "hoge" ~: 1 ~=? 2
    return ()


これらをふまえて。サンプル。

import Test.HUnit

fact n = product [1..n]

testFacts = [ "test 1!" ~:   1 ~=? fact 1
            , "test 2!" ~:   2 ~=? fact 2
            , "test 3!" ~:   6 ~=? fact 3
            , "test 4!" ~:  25 ~=? fact 4
            , "test 51" ~: 120 ~=? fact 5
            ]

main::IO ()
main = do
    counts <- runTestTT $ TestList testFacts
    putStrLn $ "Cases   : " ++ show (cases counts)
    putStrLn $ "Tried   : " ++ show (tried counts)
    putStrLn $ "Errors  : " ++ show (errors counts)
    putStrLn $ "Failure : " ++ show (failures counts)


実行結果。

 ### Failure in: 3:test 4!                 
 expected: 25
  but got: 24
 Cases: 5  Tried: 5  Errors: 0  Failures: 1
 Cases   : 5
 Tried   : 5
 Errors  : 0
 Failure : 1