Testing

Built-in test framework with effect-based mocking.

The Test Form

Loon has a built-in test framework, and it is refreshingly simple. You write tests directly in any module using the test form. No test runner to install, no framework to configure, no special file naming conventions.

LOON
[test "addition works" []
  [assert-eq [+ 1 2] 3]]

A test takes three things: a name, a parameter list (usually empty), and a body. Notice there is no fn keyword. The test form is its own special form, so you jump straight from the name to the parameters.

Assertions

Loon ships with a small, focused set of assertion functions. You will not find dozens of matchers here. These four cover the vast majority of what you need.

  • assert-eq checks that two values are equal
  • assert-ne checks that two values are not equal
  • assert checks that a boolean is true
  • assert-match checks that a value matches a pattern
LOON
[test "assertions" []
  [assert-eq 4 [* 2 2]]
  [assert-ne 3 4]
  [assert [> 10 5]]
  [assert-match [Some _] [Some 42]]]

assert-eq prints both values on failure, making debugging straightforward.

Running Tests

Run all tests in a project with loon test. You will see a summary of every test, with clear pass/fail indicators.

SHELL
$ loon test
running 12 tests
  math.add .............. ok
  math.subtract ......... ok
  string.trim ........... FAIL

11 passed, 1 failed

If you are working on a specific module and only want to run its tests, pass a pattern argument to filter by name.

SHELL
$ loon test math

Tests run in parallel by default. Use --serial for tests that need ordering.

Testing with Effects

This is where Loon's testing story gets genuinely interesting. In most languages, testing code that does IO means reaching for a mocking library, setting up dependency injection, or restructuring your code to accept interfaces. In Loon, you just use effects.

The idea is simple: if your code performs effects instead of doing IO directly, you can swap in a test handler that provides canned responses. The function under test never knows the difference.

LOON
[effect Http
  [fn get [url]]]

[fn fetch-title [url]
  [let body [Http.get url]]
  [parse-title body]]

[test "fetch-title parses html" []
  [let result
    [handle [fetch-title "http://example.com"]
      [Http.get url] [resume "Hello"]]]
  [assert-eq result "Hello"]]

Look at what happened here. The production handler for Http.get would make a real HTTP request. But in the test, we install a handler that immediately resumes with a hardcoded HTML string. The fetch-title function runs the exact same code path either way.

This pattern works for databases, file systems, randomness, or any effect. If you can define it as an effect, you can test it without mocks.

CI Integration

For CI pipelines, use loon test --format json to get machine-readable output.

SHELL
$ loon test --format json > results.json

The exit code is non-zero when any test fails, so standard CI tools work out of the box. No special adapters or reporters needed.