Haskell
test
testing
unittest
stack

Stackで単体テストを書いて実行する - Haskell入門より

このエントリは、 Haskell入門の執筆 の際に、 ページ数の都合で載せられなかった記事に加筆修正をしたものです。1.4節に載せる予定だった内容です。

このエントリの内容を試すには、 Stackをインストール し、 /tmp ディレクトリなどで以下のコマンドを実行してプロジェクトを作成してください。

$ stack new myproject
$ cd myproject/

初めての開発: 単体テストを書く

開発フローを体験するため、未実装だった単体テストを作ってみましょう。開発をするときにはREPLを使うのも便利ですが、ここでは --file-watch オプションを使います。--file-watch オプションを stack build へ指定すると、プログラムに変更があるかを監視し、変更があった場合は自動的にビルドをしてくれます。さらに、 build--test オプションを渡すと stack test と同等の機能になります。このようにしてstack build を常時走らせておくと、プログラムにエラーがあった時にすぐに見つけることができます。

$ stack build --test --file-watch
.. 略 ..

myproject-0.1.0.0: test (suite: myproject-test)

Progress: 1/2Test suite not yet implemented

Completed 2 action(s).
ExitSuccess
Type help for available commands. Press enter to force a rebuild.

単体テストを書くために、パッケージをいくつか使います。利用したresolverに登録されているパッケージを使うのが一番トラブルが少ないので、その中から使えそうなパッケージを見繕います。パッケージの一覧は Stackage のサイトで確認できます。最新のLTSを使っている場合は、 https://www.stackage.org/lts で一覧を見ることができますので、ここからパッケージを選びます。今回は HUnittest-frameworktest-framework-hunit 、 そして silently を利用してみましょう。パッケージを利用するには、 .cabal ファイルに依存関係を追加します。 myproject.cabal のテストシートのビルド設定に、パッケージを4つ追加してください。

test-suite myproject-test
  .. 省略 ..
  build-depends:       base
                     , myproject
                     -- 以下を追加
                     , HUnit
                     , silently
                     , test-framework
                     , test-framework-hunit

.cabal ファイルを変更すると、 --file-watch が変更を捉えて自動的にビルドしてくれます。追加したパッケージは、自動的にHackageからダウンロードされ、ビルドされます。

.. 略 ..

Type help for available commands. Press enter to force a rebuild.
myproject-0.1.0.0: unregistering (missing dependencies: HUnit, silently, test-framework, test-framework-hunit)
hostname-1.0: using precompiled package
mtl-2.2.1: using precompiled package

.. 略 ..

Completed 18 action(s).
ExitSuccess
Type help for available commands. Press enter to force a rebuild.

追加したパッケージを使って単体テストを書きます。自分のプログラムでパッケージを利用するには、パッケージに含まれる各モジュールを import 文で使うことを宣言します。ここでは、test/Spec.hsを以下の内容に書き換えてください。ここではコードの解説はしませんが、プログラミングの経験がある方であればなんとなく何をやっているかは想像できるのではないでしょうか。

module Main (main) where

import qualified Lib as Lib
import qualified System.IO.Silently as Silently
import qualified Test.Framework as Test
import qualified Test.Framework.Providers.HUnit as Test
import Test.HUnit ((~=?))

main :: IO ()
main = do
  captured <- Silently.capture_ Lib.someFunc
  Test.defaultMain (Test.hUnitTestToTests ("someFunc" ~=? captured))

ファイルを編集すると、 --file-watch が自動的に単体テストを走らせてくれます。

.. 略 ..

myproject-0.1.0.0: test (suite: myproject-test)

: [Failed]1/2
expected: "someFunc"
 but got: "someFunc\n"

         Test Cases  Total
 Passed  0           0
 Failed  1           1
 Total   1           1

Completed 2 action(s).
Test suite failure for package myproject-0.1.0.0
    myproject-test:  exited with: ExitFailure 1
Logs printed to console

Type help for available commands. Press enter to force a rebuild.

残念ながらテストが失敗してしまいました。 Lib.hs で利用している putStrLn は改行コードも出力するようですね。テストの方が誤ってますので、以下のようにテストを直します。

  -- .. 省略 ..
  Test.defaultMain (Test.hUnitTestToTests ("someFunc\n" ~=? captured))

これでまたテストが通るようになりました。

.. 略 ..

myproject-0.1.0.0: test (suite: myproject-test)

: [OK]ss: 1/2

         Test Cases  Total
 Passed  1           1
 Failed  0           0
 Total   1           1

Completed 2 action(s).
ExitSuccess
Type help for available commands. Press enter to force a rebuild.