本文へ移動
Hikari 仕様

std:test — テストの構築と実行

標準ライブラリモジュール (index.md)。本書中の裸の §N は本書の節を指す。言語の意味論は ../language-spec.md を参照。テストランナー (hikari test) の収集・出力は ../test.md を参照。

import t := "std:test"test / suite / timeout / run の 4 slot を持つ namespace object を t に束縛する。テストを値として構築し (test / suite / timeout)、それらを実行して集計する (run) — collect-then-run 型である。アサーションは別モジュール assert.md が担う。

ambient 束縛ではないため、REPL・playground・hikari runhikari test のいずれからも同じ API を使える。

import { test, suite, run } := "std:test"
import assert := "std:assert"

report := run [
  test "add"  { assert.equal(1 + 1, 2) }
  suite "cmp" [
    test "less"    { assert.ok ("a" < "b") }
    test "equal"   { assert.equal(7.compare 7, Equal) }
  ]
]

report.passed   #> 3
report.failed   #> 0
名前 返り値 意味
test test("name", {block}) / test "name" {block} Trial name (String) のテストを 1 件構築する。実行はしない
suite suite("name", [trials]) Trial 子 Trial (trials は Trial の List) をまとめたグループを構築する
timeout timeout(ms, trial) Trial trial の部分木に締切 ms (ミリ秒) を掛けた Trial を構築する。0 は無制限 (§4)
run run([trials]) Report Trial 列を記述順に実行し、集計した Report を返す。出力はしない

本モジュールは自身では効果を持たないtest / suite は渡したブロックの効果を呼び出し元へ透過させる(EffectConduit../language-spec.md §17.9)— 効果を持つテストを組めるのはこのためである。

1. Trial の構築 (test / suite / timeout)

  • test("name", {block})nameblock を保持する Trial 値を返す。block は必須スロット 0 個の本体付きオブジェクト ({body} 形) で、この時点では評価しない (実行は §2run)。
  • test は receiver を持ち回らない純関数である (可変セッションを共有しない)。どこで何度呼んでも、構築した Trial は他の Trial の実行に影響しない。等価は参照で見るtest(...) を 2 度書けば別の Trial で、== は偽になる (Range やハンドルと同じ扱い。../language-spec.md §9.1)。Trial は map のキーにできる。
  • suite("name", [trials]) は子 Trial をまとめたグループ Trial を返す。ネスト可能。実行時、子テスト名は name/子name のようにプレフィックスされる。
  • Trial は run に渡すための値であり、内部表現は不透明とする (name 等のスロットに依存したコードは書かない)。
  • Trial はCopyable である (../language-spec.md の複製の節)。不透明型だがホストの可変状態を持たない不変な値なので、複製は Int を複製するのと同じ意味を持つ。
  • timeout(ms, trial)trial を包んだ締切つき Trial を返す。ms は Int のミリ秒で、0 は無制限を意味する。group を包めばその部分木の単体 Trial すべてにかかる。入れ子は内側が勝つ — 外側の timeout で包んだ group の中に別の timeout があれば、その部分木には内側の値が効く。包むだけなので test / suite の呼び出しの形は変わらない。
  • Trialstd:test型メンバーとして export され、型注釈に書ける (t.Trial、または import { Trial } := "std:test")。不透明型であり内部表現は露出しない。

2. 実行と集計 (run)

  • run([trials]) は Trial 列を記述順に実行し、Report を返す。
  • 各単体 Trial について、その block を評価する。結果が panic (Error) なら FAIL (メッセージと位置を記録)、それ以外は PASS
  • 合否境界は run にある。1 件が FAIL しても残りの Trial は継続実行する。group Trial は子を再帰的に実行する。
  • run は各単体 Trial の block の評価に要した実時間を測り、Report に載せる (§4)。
  • 各単体 Trial の block の評価には締切がかかる (§4)。締切を過ぎた Trial は FAIL として数え、残りの Trial は継続実行する。
  • run出力を行わない (printer / reporter は将来課題)。整形出力は呼び出し側 (hikari testtest.md「出力と終了コード」 の形式で出力) が担う。

3. Report

run の返す Report は集計値で、少なくとも次のスロットを持つ closed・immutable なレコードである:

{
  passed: Int
  failed: Int
  duration_ms: Int
  trials: List({ name: String, passed: Bool, duration_ms: Int |})
  failures: List({ name: String, message: String, pos: String |})
|}
  • passed / failed — PASS / FAIL したテスト件数。
  • duration_ms — 実行した単体 Trial の実行時間の合計 (ミリ秒。§4)。
  • trials — 実行した単体 Trial を実行順に並べたレコード List。name は group プレフィックス込み、duration_ms はその 1 件の実行時間 (§4)。group Trial 自身は行を持たない (子が持つ)。
  • failures — FAIL した各テストの name (group プレフィックス込み)・失敗 message・発生 pos (file:line:col) のレコード List。PASS のみなら空 List。

Reportstd:test型メンバーとして export され、型注釈に書ける (../language-spec.md §13 のモジュール型 export、time.md §1 と対称)。import t := "std:test" のもとで r: t.Report と修飾参照するか、import { Report } := "std:test" で名前を取り出して r: Report と書く。不透明型の Trial と違い構造を持つ record 型なので、上のスロットは注釈越しに読める。

4. 実行時間とタイムアウト

4.1 計測

run は各単体 Trial の block の評価に要した実時間を測る。測るのは block の評価だけで、Trial の構築・収集・静的検査は含まない。値は §3trials[].duration_ms と、その合計である duration_ms に載る。run は測るだけで出力はしない (§2)。

4.2 締切

各単体 Trial には締切がかかる。既定は 60000 ミリ秒timeout(ms, trial) で包んだ部分木はその値が優先し (§1)、hikari test--timeout で既定そのものを差し替える (../test.md)。0 は無制限を意味する。

締切に達した Trial は FAIL になり、failures に次の形で載る。

  • messagetimeout: exceeded <ms> ms
  • pos — 締切に達した時点で走っていた位置

FAIL の 1 種であって独立した転帰ではない。passed / failed を読む側が、増えた区分を知らないまま素通りすることを避けるためである。

4.3 締切が効かない区間

締切は実行系が制御を持っている間だけ効く。次の区間では締切を過ぎても停止せず、その区間を抜けて実行系へ制御が戻った時点で停止する。

  • 組込の 1 呼び出しの内側
  • ホストの I/O でブロックしている間 (sleepstd:fsstd:process・未解決 Future の待ち)

したがってブロックしたまま戻らない試験は締切では止まらない。そこを止めたいなら、待つ側が Future.timeout(ms) (../prelude.md §9.6) で待ち合わせに期限を与える。

5. hikari test との関係

hikari test_test.hika の top-level に並べた bare な test "name" {block}返す Trial 値を記述順に収集し、run で実行して Report を ../test.md「出力と終了コード」の形式で整形出力する (同「収集」)。hikari testtest / suite / timeout / assert をファイルスコープに供給するため、テストファイルでは import を省略できる (REPL・hikari run では明示 import が要る)。