#Test

#Test() marks a function as a unit test. An ordinary build compiles it as a plain function and ignores the mark. A test build — rux test — collects every marked function of the package, generates a Main that runs them, and reports each one as passed or failed.

test-attribute = "#Test" "(" ")"

A test function

A test function takes no parameters, is not generic, and returns either nothing or ! TestFailure. The checks of the standard Test package return ! TestFailure, so a test hands a failed check on with ?:

import Test::{ ExpectEqual, TestFailure };

pub func Add(a: int, b: int) -> int {
    return a + b;
}

#Test()
func AddsSmallNumbers() -> ! TestFailure {
    ExpectEqual(Add(2, 3), 5)?;
}

#Test()
func AddsNegatives() -> ! TestFailure {
    ExpectEqual(Add(-2, 3), 1)?;
}

A test passes when it returns, and fails when it returns a TestFailure. A panic is not a failed test: it ends the whole test program, so the tests after it never run and rux test reports the package as failed. The package must list Test under [Dependencies] — Test = { Namespace = "Rux", Version = "*" } — because the generated runner calls Test::RunTests.

Where tests live

Tests sit beside the code they test, in the same package, so they can call functions that are not pub. That works for every package type. A library has no Main of its own, and the test build gives it one. An executable already has a Main, so it cannot hold tests: move them into a library package that the executable depends on.

rux test builds every package whose sources mark #Test() functions, plus every executable package under Tests/, and runs them all. Each test is named by its module path, AddsSmallNumbers here and Parser::ReadsNumbers for a test inside module Parser. Arguments after -- go to the runner as name filters, and a test runs when any of them matches:

rux test                                   # every test
rux test -- Adds                           # tests whose names contain "Adds"
rux test -- Adds Parser                    # tests whose names contain either
rux test -- --exact Parser::ReadsNumbers   # exactly one test

The runner also takes --skip, --list and --quiet. A package with no test the filters match is reported as skipped, and rux test fails when no package had one, so a misspelt name never passes with nothing run. A failed check prints what it expected and what it got. Change the 1 in AddsNegatives to 2 and the run reports:

running 2 of 2 tests
test AddsSmallNumbers ... ok (0.005 ms)
test AddsNegatives ... FAILED
  expected 2, but got 1

failed:
  AddsNegatives

result: FAILED. 1 passed, 1 failed, 0 skipped, 0 filtered out; finished in 0.202 ms

A test build is a build with #build.isTest set to true, so when #build.isTest can include test-only helpers.

Rules

MistakeError
arguments to the attribute'#Test' does not accept arguments
a test with parametersa '#Test' function takes no parameters
a generic testa '#Test' function cannot be generic
a test in an extend blocka '#Test' function cannot be a method
a test inside a when brancha '#Test' function cannot be declared inside 'when'
any other result type'#Test' function 'Counts' must return nothing or '! TestFailure'
tests and Main in one packagepackage 'App' declares 'Main' and '#Test()' functions; a test build generates its own 'Main'
tests without the Test dependencypackage 'App' has '#Test()' functions but does not depend on 'Test'

The last two are reported by a test build only; an ordinary build compiles the package as usual.

See also