前回は、ライブラリや実行可能ファイルを提供するパッケージをCabalで作る方法を説明しました。Cabalには、パッケージをテストするための機能も用意されています。今回はこの機能の使い方を説明します。

テスト・プログラムを用意する

 まず、Cabalのテスト機能を使って実行するテスト・プログラムを用意しましょう。単純に標準出力や標準エラー出力にメッセージを表示するプログラムでは不十分です。テストの結果をきちんとCabalのテスト機能に伝えるプログラムにする必要があります。

 第16回で説明したHUnitのrunTestTT関数、第17回で説明したQuickCheckのquickeCheck関数、verboseCheckなどは、標準出力や標準エラー出力を使ってテスト結果を報告します。このため、そのままCabalのテスト機能にテスト結果を伝えることができません。

 Cabalのテスト機能では、テストの結果を確認するために、幾つかの値を利用します。一つはプログラム終了時の「終了コード(ExitCode)」です。これは、baseパッケージのSystem.Exitモジュールで作成できます。もう一つは、バージョン1.16.0以降のCabalパッケージで用意されているDistribution.TestSuiteモジュールが提供する型の値です。

 つまり、テスト・プログラムでは、終了コードもしくはDistribution.TestSuiteモジュールが提供する型の値を使って、テストの結果を報告する必要があります。

 終了コードは、Cabalのテスト機能でテストが成功したか失敗したかを確認するには十分な情報です。しかし、Cabalのテスト機能で得られるテスト結果を確認するには不十分です。終了コードを利用する場合には、標準出力や標準エラー出力で並行してテスト結果を報告することが推奨されています。

 Cabalのテスト機能を利用するには、System.ExitモジュールやDistribution.TestSuiteモジュールなどを利用して、テスト結果をCabalのテスト機能が理解できる形式に変換する必要があります。もしくは、HUnitやQuickCheckの関数でテストを直接実行するのではなく、HackageDBで提供されている他のテスト用のライブラリやツールを使ってテストを実行する必要があります。ここでは後者の方法を説明します。

 第18回では、quickcheck-scriptやpqcを使ってQuickCheckを使ったテストを自動化する方法を説明しました。このうちpqcは、終了コードを使ってCabalのテスト機能にテスト結果を伝えられます。

 また、test-frameworkというライブラリを使うことで、HUnitやQuickCheckを使ったテストからCabalのテスト機能に、終了コードでテスト結果を報告するテスト・プログラムを作成できます。

 HUnitを使ったテストは、test-framework-hunitパッケージTest.Framework.Providers.HUnitモジュールで提供されているhUnitTestToTests関数を使うことで、test-framework用のテストに変換できます。変換されたテストをtest-frameworkパッケージTest.Frameworkモジュールで提供されているdefaultMain関数に渡すことで、test-frameworkを使ってテストを実行できます。

 例えば、第16回のHUnitを使った「モナド則を満たしているかどうか確かめるテスト」を、test-frameworkを使って実行するプログラムは以下のようになります。

●UnitTest.hs

module Main where
import Test.Framework
import Test.Framework.Providers.HUnit
import Test.HUnit

import Control.Monad.State

main = defaultMain $ hUnitTestToTests $ tests

tests = "Monad Laws" ~:
        test [ "return a >>= k  ==  k a"
               ~: runState (return 12 >>= put) 11  ~?= runState (put 12) 11
               
             , "m >>= return ==  m"
               ~: runState (get >>= return) 12 ~?= runState get 12
               
             , "m >>= (\\x -> k x >>= h)  ==  (m >>= k) >>= h"
               ~:  runState (get >>= (\x -> put x >>= return)) 12
                   ~?= runState (get >>= put >>= return) 12]

 同様にtest-framework-quickcheck2Test.Framework.Providers.QuickCheck2モジュールで提供されているtestProperty関数を使うことで、QuickCheck用のテストをtest-frameworkで実行できます。

 例えば、第17回の「quicksort関数を適用した結果のリストが整列されているかどうか確かめる」テストを、test-frameworkを使って実行するプログラムは以下のようになります。

●QuickCheckTest.hs

module Main where
import Test.Framework
import Test.Framework.Providers.QuickCheck2
import Test.QuickCheck

main = defaultMain
     [ testProperty "sorted" prop_Sorted
     ]

prop_Sorted xs = isSorted $ quicksort xs
  where types = xs::[Int]

isSorted xs = and $ zipWith (<=) xs (drop 1 xs)

quicksort  []           =  []
quicksort (x:xs)        =  quicksort [y | y <- xs, y<x ]
                        ++ [x]
                        ++ quicksort [y | y <- xs, y>=x]

 テスト・プログラムの例として、第16回で説明した型検査を使ったビルド時に行われるテストも用意しました。このテスト・プログラムは、TypeCheckモジュールを定義するTypeCheck.hsと、Mainモジュールを定義するTypeCheckTest.hsの二つのファイルから構成されます。

 先のHUnitやQuickCheckを使ったテストの例とは異なり、TypeCheckTest.hsでは終了コードもしくはDistribution.TestSuiteモジュールが提供する型の値を使いません。このテストはビルド時に行われるため、Cabalのテスト機能に実行時の結果を報告する必要はないからです。

●TypeCheck.hs

module TypeCheck where

class Render a where
    render :: a -> String

data Link = Link String URI (Maybe String) deriving Eq
newtype URI = URI String deriving Eq

link :: String -> URI -> Link
link str uri = Link str uri Nothing

linkWithTitle :: String -> URI -> String -> Link
linkWithTitle str uri title = Link str uri (Just title)

instance Render URI where
  render (URI uri) = uri

instance Render Link where
  render (Link str uri Nothing)
    = "<a href=\"" ++ render uri ++  "\">" ++ str ++ "</a>"
  render (Link str uri (Just title))
    =  "<a href=\"" ++ render uri
    ++ "\" title=\"" ++ title ++ "\">" ++ str ++ "</a>

●TypeCheckTest.hs

import TypeCheck

main = putStr "type checking is success.\n"

 ここまで説明したテスト・プログラムの例では、第16回の例とは異なり、getProgName関数を使っていません。現行のCabal 1.14.0のテスト機能では、テストに使用したプログラムの実行可能ファイルの名前を、テスト機能側で出力するようになっているためです。