Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Writing Tests

The testing module provides its forms through import-macros, the same mechanism used for all of Lykn’s macro modules. Import what you need, write tests, and the macros handle the rest.

(import-macros "testing"
  (test test-async suite step
   is is-equal is-not-equal is-strict-equal
   ok is-thrown is-thrown-async
   matches includes has obj-matches
   test-compiles))

Most test files import a subset — test and is-equal cover the majority of cases.

The Minimal Form

A test is a name and a body. No ceremony.

(import-macros "testing" (test is-equal))

(test "addition works"
  (is-equal (+ 1 2) 3)
  (is-equal (* 3 4) 12))
Deno.test("addition works", () => {
  assertEquals(1 + 2, 3);
  assertEquals(3 * 4, 12);
});

Multiple assertions in a single test are fine. The test passes when all of them pass, which is to say it fails the moment any of them doesn’t — a pattern that, in the context of software testing, passes for optimism.

Setup and Teardown

When a test needs initialisation or cleanup, use the keyword-clause syntax. The reader already knows this pattern from func’s :pre/:post/:body clauses (Ch 8) — Lykn uses the same structural conventions across func, test, and suite.

(test "database query"
  :setup    (bind db (create-temp-db))
  :teardown (close db)
  :body
    (bind result (query db "SELECT 1"))
    (is-equal result 1))
Deno.test("database query", () => {
  const db = createTempDb();
  try {
    const result = query(db, "SELECT 1");
    assertEquals(result, 1);
  } finally {
    close(db);
  }
});

The :teardown clause wraps the body in try { ... } finally { ... } — cleanup runs even when the test fails. When only :setup is present without :teardown, the setup expressions are prepended to the body with no wrapping.

Async Tests

The explicit form is test-async:

(test-async "fetches data"
  (bind result (await (fetch-data)))
  (is-equal result:status :ok))
Deno.test("fetches data", async () => {
  const result = await fetchData();
  assertEquals(result.status, "ok");
});

But test handles this automatically. If the macro finds await anywhere in the body, it emits an async function:

(test "also fetches data"
  (bind result (await (fetch-data)))
  (is-equal result:status :ok))

Same output. The test macro walks the body at compile time, spots the await, and adjusts. test-async exists for the rarer case where the body delegates to an async helper without a lexically visible await, or where the author simply wants to be explicit about it.

The skipped Lykn fences in this section are still exact surface forms. They are skipped because they are fragments of a project test file; the runnable context is supplied by a lykn new project through project.json, packages/, and test/, not by the book repository itself.