Testing

Built-in test framework with effect-based mocking.

The Test Form

Tests in Loon live where the code lives. You write them directly in any module with the test form. There is no runner to install, no framework to configure, no naming convention to obey.

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

A test is three things: a name, a parameter list (usually empty), and a body. There is no fn keyword, because test is its own special form. You go straight from the name to the parameters.

Assertions

Loon ships a small, deliberate set of assertions. There are no dozens of overlapping matchers to memorize. These four carry nearly everything you will ever need to check.

  • 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]]]

Tip

On failure, assert-eq prints both values side by side, so you see what you got and what you expected at a glance.

Running Tests

Run every test in a project with loon test. Each one reports back with a clear pass or fail.

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

11 passed, 1 failed

Focused on one module? Pass a pattern and only the tests whose names match it will run.

SHELL
$ loon test math

Note

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

Testing with Effects

This is where Loon's testing story turns genuinely interesting. In most languages, testing code that touches the outside world means reaching for a mocking library, wiring up dependency injection, or reshaping your code around interfaces. In Loon, you reach for effects you already have.

The principle is one sentence: if your code performs effects instead of doing IO directly, you can wrap it in a test handler that returns canned answers. The function under test never notices the substitution.

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"]]

Trace what happened. The production handler for Http.get would open a real connection. The test installs a handler that instead resumes with a fixed HTML string. The fetch-title function walks the identical code path in both worlds.

Tip

The same move works for databases, file systems, randomness, the clock — any effect at all. If you can name it as an effect, you can test it without mocks.

CI Integration

For pipelines, loon test --format json emits machine-readable output your tooling can parse.

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

The exit code is non-zero whenever a test fails, which is the only signal most CI systems need. No adapters, no reporters, nothing to bolt on.