Testing

Oxigen has a built-in test runner. You write test cases with the <test> angle form and run them with oxigen test.

#Writing tests

A test case is a <test>("name") { ... } block. Inside it, use expect(value) followed by a matcher to make assertions:

Oxigen
fun add(a <int>, b <int>) {
    a + b
}

<test>("addition works") {
    expect(add(2, 3)).eq(5)
    expect(add(0, 0)).eq(0)
}

<test>("comparisons and collections") {
    expect(add(2, 2)).gt(3)
    expect([1, 2, 3]).contains(2)
}

expect is provided automatically by the test runner — you do not need to introduce anything.

A test passes when its body runs without producing an error. A test fails as soon as any matcher fails (or any other runtime error occurs); evaluation of that test stops at the first failure and the message is reported.

#Matchers

expect(actual) returns an expectation with these matchers:

MatcherPasses when
.eq(expected)actual == expected
.ne(expected)actual != expected
.gt(bound)actual > bound
.gte(bound)actual >= bound
.lt(bound)actual < bound
.lte(bound)actual <= bound
.truthy()actual is truthy
.falsy()actual is falsy
.contains(item)actual (array/set/map/tuple) contains item
.is_value()actual is a <Value>-wrapped value
.is_error()actual is an <Error>-tagged error value

The matchers are implemented in pure Oxigen in stdlib/test.oxi, so you can read or extend them.

#Running tests

sh
oxigen test                 # discover and run every *_test.oxi under the cwd
oxigen test math_test.oxi   # run a single file
oxigen test ./tests         # run every *_test.oxi under a directory

Discovery is recursive and skips hidden directories, target/, and node_modules/. By convention, test files end in _test.oxi.

Example output (the [ok]/[fail] markers are shown in green/red when the terminal supports color; plain when piped or redirected):

math_test.oxi
  [ok]   addition works
  [fail] comparisons and collections
       expected 5 but got 4

test result: FAILED. 1 passed; 1 failed

oxigen test exits with a non-zero status if any test fails, so it works in CI. Color is suppressed when stdout is not a terminal or when NO_COLOR is set.

A few things count as failures so a broken suite can't pass by accident:

  • Calling os.exit() in a test, or at a test file's top level, fails that test instead of ending the run.
  • A <test> block inside main { } or a function never runs, so it is reported as a failure telling you to move it to the top level.
  • A test file with no tests whose top level fails (say x := 1 / 0) is reported as a failing <top level> entry.

Tests can use diverge/converge: tasks run against the test's own program.

#Tests and normal runs

<test> blocks only execute under oxigen test. Running a file the normal way (oxigen file.oxi) skips every <test> block, so you can keep tests next to the code they exercise without affecting production runs.