SMLUnitの出力をTAP形式にするテストランナーを書いた

SMLUnitをSML#0.90からサルベージgithubに上げたユニットテストライブラリに、TAPTestRunnerを実装しました。(githubリポジトリ(tap_supportブランチ))


TestRunnerというのは、SMLUnitを使って定義されたテストを実行して結果をダンプしてくれるモジュール*1です。
デフォルトでは前回の使用例でも出てきている、SMLUnit.TextUITestRunner が用意されています。(HTMLReportTestRunnerという名前だけはコード中に残っているのでSML#公式サイトのどっかにあるのかも…)


今回実装した TAPTestRunner はテストの実行結果をTAP形式で出力してくれるモノです。テストの内容自体を変更する必要は一切ありません。 TextUITestRunner を使っていた部分を TAPTestRunner に置き換えれば使うことが出来ます。

実行結果

以下にサンプルテストの実行例を示します。(* *) で囲まれた部分は説明のために後から書き足した部分です。

0..13 (* テストの総数 *)
ok 1
not ok 2 expected:<0>, actual:<1> (* 失敗すると、可能な場合は要求する値と実際の値を表示する *)
# Test
ok 4
not ok 6 expected:<SOME("hoge")>, actual:<SOME("hog")>
# Test.nest1.nest2 (* 失敗した場合はテスト階層を表示してくれる *)
not ok 5 expected:<[...{1}..., "bar", ...]>, actual:<[...{1}..., "BAR", ...]>
# Test.stringlist
not ok 5 expected:<true>, actual:<false>
# Test
ok 7
not ok 7 expected:<1>, actual:<2>
# Test
not ok 8 expected:<2.0>, actual:<0.0>
# Test
not ok 9 # TODO 後で頑張る (* TODO ディレクティブが指定可能 *)
# Test
ok 11 # SKIP テストにはまだ早い (* SKIP ディレクティブも指定可能 *)
# Test.skip demo
ok 11
not ok 13 expected:<true>, actual:<false>
# Test.sum[1..10]=55
val it = () : unit

TODOSKIP はTAPで指定できるキーワードで、専用の構文(#の後ろにTODOって書くだけですが)で表示してくれます。
これらは今回追加した TAPTestRunner.TODO(またはSKIP) 例外をテスト内で投げることで使用することが出来ます。
TODO を使った場合は必ず not ok と扱われ、SKIP を使った場合は必ず ok として扱われます。

注意

TextUITestRunner でもこれらを使うテストは実行できますが、どちらを使う場合でも(TextUITestRunnerが知らない例外なので)テストケース内で発生したエラーとして扱われますので注意してください。その他のテストは正常に実行&ダンプされます。

テストコードサンプル

上の実行例で使用したテストコードです。前回より少し凝ったものになっています :)

(* CM.make "$/smlunitlib.cm"; use "test.sml"; *)
structure Test =
struct
  open SMLUnit
  open Assert
  val ($,&,%) = (Test.TestLabel,Test.TestList,Test.TestCase)
  val ? = assertTrue

  fun fizzbazz () = ["foo","BAR","bazz"]

  fun test () =
    $ ("Test" (* 全体に名前付ける *)
    , &[ % (fn()=> assertEqualInt 0 0) (* 0==0 *)
       , % (fn()=> assertEqualInt 0 1) (* 0==1 *)
       , $ ("nest1", (* option のアサートを組み立てて使う *)
           &[ % (fn()=> assertEqualOption assertEqualInt (SOME 314) (SOME (300 + 14)))
           , $ ("nest2",
               &[ % (fn()=> assertEqualOption assertEqualString
			                                  (SOME "hoge")
			                                  (SOME (implode [#"h",#"o",#"g"])))
                ])
            ])
       (* string list のアサートを組み立てて使う *)
       , $ ("stringlist"
           , % (fn()=> assertEqualList assertEqualString ["foo","bar","bazz"] (fizzbazz()))
           )
       , % (fn()=> ? (1=0))
	   (* ケースに名前を付ける *)
       , $ ("1==1", % (fn()=> assertEqualInt 1 1))
       , % (fn()=> assertEqualInt 1 2)
	   (* 特殊化された比較アサート *)
       , % (fn()=> assertEqualReal 2.0 0.0)
       (* TODO *)
       , % (fn()=> raise TAPTestRunner.TODO "後で頑張る")
       (* SKIP *)
       , $ ("skip demo", % (fn()=> raise TAPTestRunner.SKIP "テストにはまだ早い"))
       , % (fn()=> ? (6 = foldr op+ 0 [1,2,3]))
       , $ ("sum[1..10]=55",
            % (fn()=> ? (55 = foldr op+ 0
                            (List.tabulate(10,fn i=> i)))))
      ])
end
structure T = SMLUnit.TAPTestRunner;
val _ = T.runTest {output=TextIO.stdOut} (Test.test()); (* テスト実行 *)

Platform

SML/NJ, MLton, SML#1.2.0 でサンプルの実行を確認しています。

*1:SML的にはTESTRUNNERシグネチャを持つストラクチャ