OxL Reference

Step 10. Tests

Why#

If tests are a separate framework with its own conventions, they get written less often. In OxL test is a language declaration: the compiler knows about tests, leaves them out of the ordinary build and checks them together with the rest of the code. assert prints the values of all subexpressions when it fails, so a failure message is usually clear without a debugger.

The first test#

A test is test "name" { … } next to the code or in the tests/ directory. Run it with oxl test (for one file: oxl test file.oxl).

oxl✓ checked in CI
fn clamp(x: i32, lo: i32, hi: i32) -> i32  pure => if x < lo { lo } else if x > hi { hi } else { x }

test "clamp keeps values inside the range" {
  assert_eq(clamp(5, 0, 10), 5)
  assert_eq(clamp(-3, 0, 10), 0)
  assert_eq(clamp(99, 0, 10), 10)
}

test "table-driven" {  // table-driven: data plus a loop
  let cases = [(1, 0, 3, 1), (-5, 0, 3, 0), (7, 0, 3, 3)]
  for (x, lo, hi, want) in cases {
    assert_eq(clamp(x, lo, hi), want)
  }
}

test fuzz "clamp stays in range" (x: i32, lo: i32) {  // fuzzing: the runtime generates the arguments
  let hi = lo.saturating_add(10)
  let c = clamp(x, lo, hi)
  assert c >= lo && c <= hi
}

Output of oxl test:

oxl
running 2 tests
test main::clamp keeps values inside the range ... ok
test main::table-driven ... ok

test result: ok. 2 passed; 0 failed; 0 skipped

Walkthrough#

  • assert_eq(a, b) prints both sides on a mismatch; assert cond, assert x is T, assert_ne, assert_fails(expr, ErrorType) and assert_snapshot(&value) are the other assertions (§18.2).
  • A table-driven test is an ordinary list of tuples and a for loop: no special syntax is needed. Add a row to the table and you get a new case.
  • test fuzz "…" (x: i32, lo: i32) is fuzzing: the runtime generates the test parameters (respecting refined types), and a failing input is saved for a rerun (oxl test --kind fuzz, guide).
  • The body of a test may use ? - an error fails the test and prints the error.

Fixtures, mocks and simulation#

  • fixture name() -> T { …; yield value; …cleanup } and test "…" with name as x { … } - set up and tear down resources.
  • mock Service { method(pattern) => answer once, then … } replaces a service, Service.calls(.method) records the calls.
  • test sim "…" seeds 1..=300 is a deterministic simulation with a virtual clock and network: an hour passes instantly, and a failing seed is reproduced with oxl test --seed N.

Examples of all three are in the tour of testing and in the recipes.

Common mistake: parameters on an ordinary test#

Parameters are allowed only on test fuzz. An ordinary test with a parameter is the error test-decl:

oxl✓ checked in CI
test "sum" (x: i64) {
  assert x + 0 == x
}
error
test-decl

Replace test "sum" with test fuzz "sum".

Remember#

  • test is a language declaration; oxl test runs tests in parallel.
  • Table-driven tests are a loop over a list; fuzzing is test fuzz with parameters.
  • Keep logic pure: it is the easiest to test (step 8).

Next: concurrency.

See also

OxL 1.0.0-beta · Documentation version 1.0.0-beta© OxwaldLink · generated from the Standard, the std sources and the guides