Test-Suiteでパッケージをテスト

 では、Cabalのテスト機能を使ってパッケージをテストしてみましょう。

 パッケージで提供するライブラリや実行可能ファイル以外に機能テストで利用するプログラムは、*.cabalファイルのtest-suiteという項目に記述します。これは、Cabal 1.10およびcabal-install 0.10.0から提供されている機能です。cabal-version:フィールドは、1.10以降に設定しておきましょう(参考リンク1参考リンク2)。

Test-Suite Example
  type:       exitcode-stdio-1.0
  main-is:    Example.hs
  hs-source-dirs:  examples
  default-language: Haskell2010
  build-depends:    base < 5

 test-suiteには、テストの種類を指定するtype:という特別なフィールドがあります。それ以外は、前回説明したexecutableと同じように書けます。

 type:フィールドには、テストで使うプログラムの形式を指定します。プログラムの形式には、exitcode-stdio-1.0とdetailed-0.9の2種類があります。exitcode-stdioは「終了コードを使ってテストの成功や失敗をCabalのテスト機能に伝え、人間が読めるテスト結果を標準出力や標準エラー出力で報告する形式」です。detailedは「CabalパッケージのDistribution.TestSuiteモジュールを使って、Cabalのテスト機能が解釈できる詳しい結果を報告する形式」です。exitcode-stdioやdetailedの後に続く0.9や1.0は、それぞれの形式のバージョンです。テストに使われるプログラムの形式は、<種類名>-<バージョン番号>の形で記述します。

 type:フィールドにexitcode-stdio-1.0を指定した場合、前節で用意した「終了コードを使ってテストの結果を伝えるプログラム」をテスト・プログラムとしてコンパイル・実行します。広く使われているtest-frameworkやpqcなどのテストを自動化するライブラリは、たいていこの形式になっています。

 type:フィールドにdetailed-0.9を指定した場合には、「CabalパッケージのDistribution.TestSuiteモジュールで提供されている型の値を使って、テスト結果を報告するプログラム」を用意する必要があります。このプログラムは、main-is:フィールドで指定したファイルのmain関数ではなく、test-module:フィールドで指定したモジュールのtests :: IO [Test]関数から実行される形で記述しなければなりません。

 残念ながら、2012年12月時点では、detailed-0.9向けのライブラリはまだ提供されておらず、対応したテスト・プログラムを自力で書かなければなりません。このため、今回はdetailed-0.9の使い方は詳しく説明しません。Cabalのテスト機能ではdetailed-*という形式が利用可能なことを知っていれば今は十分です。

 では、テスト・プログラムを実行してみましょう。*.cabalファイルのtest-suiteに、用意したテスト・プログラムである「TypeCheckTest」「UnitTest」「QuickCheckTest」を記述します。

name:           Test
version:        0.0
synopsis:       Cabal test example
description:    Up to date version of testing example
license:        BSD3
maintainer:     shelarcy <shelarcy@example.com>
category:       Testing
build-type:     Simple
cabal-Version:  >= 1.10
Library
  exposed-modules:  TypeCheck
  default-language: Haskell2010
  build-depends: base < 5, mtl, directory

Test-Suite TypeCheckTest
    type:             exitcode-stdio-1.0
    main-is:          TypeCheckTest.hs
    hs-source-dirs:   test
    default-language: Haskell2010
    build-depends: base < 5, Test

Test-Suite UnitTest
    type:             exitcode-stdio-1.0
    main-is:          UnitTest.hs
    hs-source-dirs:   test
    default-language: Haskell2010
    build-depends: base < 5, Test
                 , directory, mtl, HUnit
                 , test-framework, test-framework-hunit

Test-Suite QuickCheck
    type:             exitcode-stdio-1.0
    main-is:          QuickCheckTest.hs
    hs-source-dirs:   test
    default-language: Haskell2010
    build-depends: base < 5, Test
                 , directory, mtl, QuickCheck > 2
                 , test-framework, test-framework-quickcheck2

 テスト・プログラムを作成するためのtest-suiteに加え、ライブラリを作成するためのlibraryという項目を使っている点に注意してください。Cabalではテスト・プログラムだけのパッケージは作れません。Cabalでは、test-suiteという項目を使って記述したテスト・プログラムに加え、「executableという項目を使って記述した実行可能ファイル」か「libraryという項目を使って記述したライブラリ」をパッケージの中身として提供する必要があります。

$ cabal configure --enable-tests
Resolving dependencies...
Configuring Test-0.0...
Error: No executables and no library found. Nothing to do.

$ cabal check
The package will not build sanely due to these errors:
* No executables and no library found. Nothing to do.

Hackage would reject this package.

 通常は、libraryを使って提供するライブラリにtest-suiteを使ってテスト・プログラムを付属させるのがよいでしょう。ライブラリにtest-suiteを使ってテスト・プログラムを付属させる方が、モジュールの参照が容易なためです。

 executable(もしくはlibrary)のother-modules:フィールドで指定したモジュールは、test-suiteのother-modules:フィールドで指定しない限り、テスト・プログラムのソースコードから参照できません。

 一方、libraryのexposed-modules:フィールドで指定したモジュールは、test-suiteのbuild-dependeds:フィールドで依存パッケージを指定することにより参照できます。test-suiteのbuild-depdendsフィールドでは、test-suiteの記述が含まれる*.cabalファイルで定義するパッケージ自身を、依存パッケージとして挙げることが可能です。これにより、test-suiteのother-modules:フィールドで個々のモジュールを指定する手間を省けます。

 この例ではモジュール名が短く、libraryのexposed-modules:フィールドで指定するモジュールが少ないため、モジュール名を指定する手間を省く恩恵はあまりありません。しかし、階層化された長いモジュール名を使ったり、多くのモジュールを指定したりする必要がある場合には、other-modules:フィールドを使ったモジュール指定を省略できるかどうかは大きな差になります。

 なお、テスト・プログラムのソースコードは、ディレクトリ直下ではなく、hs-source-dirs:フィールドを使って指定したサブディレクトリに入れてください。テスト・プログラムのソースコードがライブラリや実行可能プログラムのソースコードと同じディレクトリに存在すると、混乱の元になります。また、Cabalのテスト機能には一部バグがあり、ライブラリのソースコードとテスト・プログラムのソースコードが同じディレクトリにあるとうまく動かないことがあります(参考リンク)。

 test-suiteに記述したテスト・プログラムは、configureコマンドの--enable-testsオプションを使って有効化します。この結果、buildコマンド実行時のビルド対象になり、testコマンドで実行できるようになります。

$ cabal configure --enable-tests
Resolving dependencies...
Configuring Test-0.0...

$ cabal build
Building Test-0.0...
Preprocessing library Test-0.0...
[1 of 1] Compiling TypeCheck        ( TypeCheck.hs, dist/build/TypeCheck.o )
In-place registering Test-0.0...
Preprocessing test suite 'UnitTest' for Test-0.0...
[1 of 1] Compiling Main             ( test/UnitTest.hs, dist/build/UnitTest/UnitTest-tmp/Main.o )
Linking dist/build/UnitTest/UnitTest ...
Preprocessing test suite 'TypeCheckTest' for Test-0.0...
[1 of 1] Compiling Main             ( test/TypeCheckTest.hs, dist/build/TypeCheckTest/TypeCheckTest-tmp/Main.o )
Linking dist/build/TypeCheckTest/TypeCheckTest ...
Preprocessing test suite 'QuickCheck' for Test-0.0...
[1 of 1] Compiling Main             ( test/QuickCheckTest.hs, dist/build/QuickCheck/QuickCheck-tmp/Main.o )
Linking dist/build/QuickCheck/QuickCheck ...

$ cabal test
Running 3 test suites...
Test suite QuickCheck: RUNNING...
Test suite QuickCheck: PASS
Test suite logged to: dist/test/Test-0.0-QuickCheck.log
Test suite UnitTest: RUNNING...
Test suite UnitTest: PASS
Test suite logged to: dist/test/Test-0.0-UnitTest.log
Test suite TypeCheckTest: RUNNING...
Test suite TypeCheckTest: PASS
Test suite logged to: dist/test/Test-0.0-TypeCheckTest.log
3 of 3 test suites (3 of 3 test cases) passed.

 用意した三つのテストが成功しているのがわかります。テストが成功した場合には、テスト・プログラムが標準出力や標準エラー出力に渡した結果は表示されません。標準出力や標準エラー出力に渡された詳しいテスト結果を知りたい場合には、生成されるdist/test/<パッケージ名>-<バージョン番号>-<テスト・プログラム名>.logを見てください。

 テスト・プログラムのうち、UnitTestを失敗するように書き換えた場合、結果は以下のようになります。

$ cabal test
Running 3 test suites...
Test suite QuickCheck: RUNNING...
Test suite QuickCheck: PASS
Test suite logged to: dist/test/Test-0.0-QuickCheck.log
Test suite UnitTest: RUNNING...
:Monad Laws:
  :return a >>= k  ==  k a: [Failed]
expected: ((),12)
 but got: ((),1)
  :m >>= return ==  m: [OK]
  :m >>= (\x -> k x >>= h)  ==  (m >>= k) >>= h: [OK]

         Test Cases  Total
 Passed  2           2
 Failed  1           1
 Total   3           3
Test suite UnitTest: FAIL
Test suite logged to: dist/test/Test-0.0-UnitTest.log
Test suite TypeCheckTest: RUNNING...
Test suite TypeCheckTest: PASS
Test suite logged to: dist/test/Test-0.0-TypeCheckTest.log
2 of 3 test suites (2 of 3 test cases) passed.

 UnitTestが失敗していることを示すFAILというメッセージが表示されていることがわかります。またUniteTestのFAILというメッセージの直前には、テストの具体的な結果が表示されています。

 このように、失敗したテストでは、実行中を示すRUNNINGとテスト失敗を示すFAILの間に、テスト・プログラムが標準出力や標準エラー出力に渡した結果が表示されます。