12 — Writing tests

Previous: Modules and packages

日本語版: 12_tests.vibe.md

A test is a test "name" { ... } block written in an ordinary source file. There is no framework to install and nothing to import.

fn double(n: Int) -> Int {
  n * 2
}

test "double works" {
  assert_eq(double(21), 42)
  assert(double(0) == 0)
}

By convention they live in files ending _test.vibe, so that the build can leave them out of your shipped package.

vibe test demo_test.vibe             # one file
vibe test a_test.vibe b_test.vibe    # several
vibe test tests/                     # every *_test.vibe underneath

A passing file reports each file and a one-line summary:

$ vibe test demo_test.vibe
ok   demo_test.vibe
[vibe-test] 1 passed, 0 failed (1 files, 1 tests)

When one fails

Change the expected value to 43 and the report names the test, shows both sides, and stops:

$ vibe test demo_test.vibe
FAIL demo_test.vibe
       failing test: double works
       assert_eq failed
         at off=62
         expected: 43
         actual:   42
[vibe-test] 0 passed, 1 failed (1 files, 1 tests)

at off=62 is the assert's own position, so two asserts with the same expected value in one test are told apart (#2202). It is a BYTE offset, not a line number -- every position in vibe is a byte offset (ADR-0108).

assert_eq(actual, expected) works for any type that can be compared, and compares strings by content — so you can assert directly on a concatenation or on whatever a function returned, without converting anything first. assert(cond) is for a plain Bool.

inspect — let the tool write the expectation

Often you know what a value should look like but do not want to type it out, and you especially do not want to retype it every time the format changes legitimately. inspect puts the expected rendering in the source, and the tool maintains it:

fn double(n: Int) -> Int {
  n * 2
}

test "inspect records the value" {
  inspect(double(3), "6")
}

Run with --update and any stale expectation is rewritten to what the code actually produced:

vibe test --update demo_test.vibe

Then you read the diff. That is the workflow: the tool proposes, you review. It is much better than hand-maintaining a string, and much worse than assert_eq if you have not read the diff — a snapshot you accepted without looking is a test that asserts nothing.

Use assert_eq when you know the answer and it matters. Use inspect for the shape of a bigger structure, where writing it out by hand is the only thing stopping you from testing it at all.

A test grants what it uses

A test, bench or example block is an entry point, the same way fn main is. The row after the name is a grant, so the keyword is allows, not with. It widens the row a test already runs with (Fs, Console, Exception, and the other defaults assert needs). A capability that is not in that set, such as Http::request, has to be named or the call does not compile:

test "hits the network" allows Http::request {
  let _ = Http::request("GET", "http://127.0.0.1:9/", "", "")
  assert(true)
}

An anonymous test { } has no name to hang a row on, so it cannot grant one. Capabilities is why allows and with are different words.

Testing what you cannot call directly

Tests live inside the package, so they can reach its internals — a test file may use modules that consumers cannot, which is what lets you test a helper without exporting it just for the test. This is the reason tests belong beside the code rather than in a separate tree.

Next: Collections.