Skip to content

Tests

Lisette’s test runner finds #[test] functions in .test.lis files and runs them with lis test.

lis test compiles and runs every test in the project. In the test report, tests are grouped by package and file:

Terminal window
✓ Compiled demo v0.1.0 (18ms)
src/math/
math.test.lis
├── addition
├── subtraction
├── multiplication
└── division
4 passed (122ms)

On failure, the test report includes a Failures section:

Terminal window
✓ Compiled demo v0.1.0 (7ms)
src/math/
math.test.lis
├── addition
├── subtraction
├── multiplication
└── division
Failures
addition
╭─[src/math/math.test.lis:3:10]
1 │ #[test]
2 │ fn addition() {
3 │ assert add(2, 2) == 5
· ───────┬──────
· ╰── 4 is not equal to 5
4 │ }
5
╰────
1 failed · 3 passed (94ms)

lis test exits non-zero when any test fails, or when a run finishes without having executed any test.

Test files end with .test.lis and come in two kinds.

  • Internal tests sit in the same dir as the logic they test, so internal tests can access all private symbols in that package.

  • External tests sit in a tests/ dir at project root. External tests import packages like any other consumer, so external tests can access only public symbols. Integration tests belong here.

  • Directorysrc/
    • main.lis
    • Directorygeometry/
      • geometry.lis
      • geometry.test.lis internal
      • shapes.lis
      • shapes.test.lis internal
  • Directorytests/
    • geometry.test.lis external
    • Directoryintegration/
      • dimensions.test.lis external
      • roundtrip.test.lis external

lis check includes test files and production code. lis build and lis run exclude test files from the emitted output.

In a test file, a function marked #[test] is found and invoked by the test runner. A test function usually has no parameters and no return type.

#[test]
fn addition() {
assert add(2, 2) == 4
}

Optionally, a test can also carry a title and description:

/// Surrounding whitespace is trimmed before the count is taken.
#[test("counts fields in a CSV row")]
fn counts_fields() {
assert field_count(" a , b ") == 3
}

The function title replaces the function name in the report, and the description follows beneath:

Terminal window
Failures
counts fields in a CSV row
Surrounding whitespace is trimmed before the count is taken.
╭─[src/math/math.test.lis:10:10]
8 │ #[test("counts fields in a CSV row")]
9 │ fn counts_fields() {
10 │ assert field_count(" a , b ") == 3
· ──────────────┬─────────────
· ╰── 2 is not equal to 3
11 │ }
╰────
1 failed (111ms)

lis test --filter <pattern> matches against both function names and function titles.

In a test function, the assert keyword checks if a boolean expression is true or fails the test.

#[test]
fn basics() {
assert is_email("name@example.com")
assert is_email("not-an-email")
}

On failure:

Terminal window
Failures
basics · assertion failed
╭─[src/math/math.test.lis:10:10]
8 │ fn basics() {
9 │ assert is_email("name@example.com")
10 │ assert is_email("not-an-email")
· ────────────┬───────────
· ╰── assertion failed
11 │ }
╰────
1 failed (132ms)

To compare types that are not comparable with ==, mark them with #[equality] and use the equals method.

See Equality

#[equality]
struct Order {
id: int,
tags: Slice<string>,
}
#[test]
fn orders_match() {
let a = Order { id: 1, tags: ["a"] }
let b = Order { id: 9, tags: ["z"] }
assert a.equals(b)
}

On failure:

Terminal window
Failures
orders_match
╭─[src/math/math.test.lis:11:10]
9 │ let a = Order { id: 1, tags: ["a"] }
10 │ let b = Order { id: 9, tags: ["z"] }
11 │ assert a.equals(b)
· ─────┬─────
· ╰─┤ left: Order { id: 1, tags: ["a"] }
· right: Order { id: 9, tags: ["z"] }
12 │ }
╰────
1 failed (102ms)

Use let assert to assert by pattern matching:

#[test]
fn parses_header() {
let bytes: Slice<byte> = [0x02, 0x00]
let assert Ok(h) = parse_header(bytes)mismatch fails the test
assert h.version == 2
}

To assert that an expression panics, recover from it and match the Err.

#[test]
fn panics_out_of_bounds() {
let xs = [1, 2, 3]
let assert Err(_) = recover { xs[9] }non-panic fails the test
}

Use Result to assert via the ? operator:

#[test]
fn round_trips() -> Result<(), error> {
let point = Point { x: 1, y: 2 }
let bytes = encode(point)?Err fails the test
let restored = decode(bytes)?Err fails the test
assert restored == point
Ok(())
}

A test can take a t parameter of type TestContext, which works similarly to Go’s testing.T.

For example, use t.run to group assertions into named subtests:

fn cases() -> Slice<Case> {
[
Case { name: "a", input: 1, expected: 2 },
Case { name: "b", input: 2, expected: 4 },
]
}
#[test]
fn basics(t: TestContext) {
for case in cases() {
t.run(case.name, |_| {
assert compute(case.input) == case.expected
})
}
}

If omitted, the type of t is inferred:

#[test]
fn basics(t) {
for case in cases() {
t.run(case.name, |_| {
assert compute(case.input) == case.expected
})
}
}

t is also available as a lambda parameter:

#[test]
fn basics(t) {
for case in cases() {
t.run(case.name, |t| {
t.parallel()mark concurrent
assert compute(case.input) == case.expected
})
}
}

t.skip() stops a test early and records why.

#[test]
fn status_ok(t: TestContext) {
if !online() {
t.skip("service offline")
}
assert fetch_status() == 200
}

On skipping:

Terminal window
src/math/
math.test.lis
└── status_ok (service offline)
1 skipped (117ms)

t.log(value) displays a value for debugging:

#[test]
fn computes_total(t: TestContext) {
let total = add(20, 22)
t.log(total)
assert total == 42
}

Logged values appear in a Logs section:

Terminal window
Logs
» computes_total
╭─[src/math/math.test.lis:4:9]
2 │ fn computes_total(t: TestContext) {
3 │ let total = add(20, 22)
4 │ t.log(total)
· ──┬──
· ╰── 42
5 │ assert total == 42
6 │ }
╰────
1 passed (99ms)

Use --go-flags to pass flags through to go test.

Terminal window
lis test --go-flags "-failfast"

Useful Go flags:

FlagEffect
-failfastStop at the first failing test
-raceEnable the data race detector
-timeout 30sFail the run if it exceeds the given duration

To select tests by name, use lis test --filter rather than -run.

In a library project, use import "root" to externally test files directly under src/.

  • Directorysrc/
    • geo.lis
    • Directoryshapes/
      • shapes.lis
  • Directorytests/
    • api.test.lis

In tests/api.test.lis:

import "root"
import "shapes"
#[test]
fn distance_is_symmetric() {
assert root.distance(2, 9) == root.distance(9, 2)
}
#[test]
fn square_has_four_sides() {
assert shapes.sides(shapes.square()) == 4
}