# 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:

```oxi
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:

| Matcher | Passes 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.
