テスト
テストは @test を付けた関数で、検証は assert で書きます。外の世界にはエフェクトを通して触れるので、ハンドラを差し替えるだけでテストになります。モックのための仕組みはありません。
import Std.Console.{Console, SystemConsole, TestConsole}import Std.State.{State}
type User(id: Int, name: String)
error NotFound(id: Int)
effect Users { fn find(id: Int) -> User fails NotFound}
fn greet(id: Int) -> String fails NotFound use { Users, Console } { user = (use Users).find(id) (use Console).print_line("Hello, #{user.name}!") user.name}
handler FakeUsers implements Users { fn find(id) { if id == 1 { User(id:, name: "alice") } else { fail NotFound(id:) } }}
fn main() { with SystemConsole, FakeUsers { greet(1) () }}
@testfn greet_returns_the_name() { (name, lines) = State.run([], fn() { with TestConsole, FakeUsers { greet(1) } }) assert name == "alice" assert lines == ["Hello, alice!"]}
@testfn greet_fails_for_unknown_id() { with SystemConsole, FakeUsers { result = Some(greet(999)) escape { NotFound(_) -> None } assert result == None }}$ emela test main.emelrunning 2 teststest greet_returns_the_name ... oktest greet_fails_for_unknown_id ... ok
test result: ok. 2 passed; 0 failed; 0 filtered outgreet はテストのことを知りません。本番では with に本物のハンドラを、テストでは FakeUsers と TestConsole を書きます。
@test と assert
Section titled “@test と assert”@test は引数のない fn に付けます。どのファイルにも書けます。実装と同じファイルに書けば、非公開の関数をそのままテストできます。テスト関数は通常のビルドでは取り除かれるので、呼べるのはほかのテスト関数の中からだけです。
assert 式 は、式が False のとき defect になります。
error NotFound(id: Int)
fn find(id: Int) -> String fails NotFound { if id == 1 { "alice" } else { fail NotFound(id:) }}
fn add(a: Int, b: Int) -> Int { a + b + 1 }
fn main() { () }
@testfn add_works() { assert add(1, 2) == 3}
@testfn find_alice() { assert find(1) == "alice"}
@testfn find_bob() { assert find(2) == "bob"}
@testfn not_empty() { xs = [1, 2] assert List.is_empty(xs)}
@testfn crashes() { panic("boom")}$ emela test main.emelrunning 5 teststest add_works ... FAILEDtest find_alice ... oktest find_bob ... FAILEDtest not_empty ... FAILEDtest crashes ... FAILED
failures:
---- add_works ----defect: assertion failed: add(1, 2) == 3 left: 4 right: 3
---- find_bob ----error: NotFound(id: 2)
---- not_empty ----defect: assertion failed: List.is_empty(xs)
---- crashes ----defect: boom
failures: add_works find_bob not_empty crashes
test result: FAILED. 1 passed; 4 failed; 0 filtered out$ echo $?1失敗のメッセージは assertion failed: <式の字面> で、比較のときは続けて両辺の値(left: と right:)を出します。assert はキーワードで、コンパイラが式の形を見ます。assert_eq のような関数を使い分ける必要はありません。テスト関数の外でも使え、main の中で失敗すれば defect(終了コード 101)です。
テスト関数の中で処理されなかったエラー(上の find_bob)と defect(上の crashes)は、そのテストの失敗として報告され、ほかのテストは続けて実行されます。失敗することを確かめるときは、最初の例の greet_fails_for_unknown_id のように escape で値に変えて assert します。
ハンドラを差し替える
Section titled “ハンドラを差し替える”テストで使うハンドラは普通のハンドラと同じように書きます。上の FakeUsers がそれです。同梱のエフェクトには、状態を State に置くテスト用のハンドラがあります。State.run で最初の状態を渡し、終わった後の状態を見て確かめます。
| ハンドラ | 状態 | 説明 |
|---|---|---|
TestConsole | State[List[String]] | print_line の行を順に貯める。print_error_line は捨てる |
TestClock | State[Instant] | 仮の時刻で動く。実際の時間は待たない |
TestFileSystem | State[TestFiles] | ファイルとディレクトリを状態に置く |
TestHttp | State[TestServer] | respond の関数が応答を返し、送った要求を sent に貯める |
使い方は Platform、FileSystem、Http にあります。State そのものは 状態と資源 で扱います。
TestClock の sleep は、どの fiber も先に進めなくなったとき、一番早く起きる予定の時刻まで時刻を進めます。fiber を使うコードも、実際の時計と同じ順で、待たずにテストできます。
import Std.Clock.{Clock, TestClock}import Std.Fiber.{Fiber}import Std.Instant.{Instant}import Std.State.{State}
fn slow() -> Int use { Clock } { (use Clock).sleep(5000) 1}
@testfn gives_up_after_100ms() { start = Instant.from_unix_milliseconds(0) (result, end) = State.run(start, fn() { with TestClock { Fiber.timeout(100, fn() { slow() }) } }) assert result == None assert Instant.to_unix_milliseconds(end) == 100}
fn main() { () }複数の State を重ねるときは State.run を入れ子にします。with Platform, TestClock { ... } のように、layer の後ろに書いたハンドラで一部だけを置き換えることもできます。
テスト関数の中で使うエフェクトは、Async を除いてすべて with で供給します。Async はテストランナーが供給するので、sleep や fiber はそのまま使えます。
import Std.Console.{Console}
@testfn prints() { (use Console).print_line("hi")}
fn main() { () }error: effect `Console` is not provided in test function `prints`emela test
Section titled “emela test”emela test [パス] [フィルタ]パスはエントリのファイルかプロジェクトのディレクトリで、省くと . です。フィルタを渡すと、名前に部分一致するテストだけを実行します。1つでも失敗すれば終了コード 1、なければ 0 です(テストが0個でも 0 です)。
テストの名前は、エントリのモジュールのものは関数名だけ、ほかのモジュールのものはモジュール名を前に付けます。src/main.emel と src/text/slug.emel のプロジェクトでは次のようになります。
$ emela testrunning 2 teststest main_slug ... oktest Text.Slug.joins_words ... ok
test result: ok. 2 passed; 0 failed; 0 filtered out$ emela test . Slugrunning 1 testtest Text.Slug.joins_words ... ok
test result: ok. 1 passed; 0 failed; 1 filtered outほかのオプションは CLI にあります。