Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 1 Nov 2026. What's built · Install 0.2.0
jadpo
Guides: all 11

Guides

Write tests

You end with action and route tests, fixtures, a fixed clock and tests signed in as someone.

On this page

Tests in Jadpo sit beside the code they test, and each one gets a PostgreSQL database of its own. Test files end .test.jadpo, and a test's database is made from your migrations and thrown away after it, so tests never see each other's data and can run in any order. This guide tests a reading list: an action on its own, then its routes, signed in as different people.

You start from an app where each Reader signs in and keeps a list of Entry records, which only they may read and change, served at POST /entries, GET /entries and POST /entries/{entry_id}/finish. The action it tests is this one:

domains/reading/actions/finish_entry.jadpo:

jadpo
use reading/entities { Entry, EntryNotFound }use reading/types { EntryView }/// Marks a book on the list as read, now.action finish_entry(entry_id: Entry.id) fails EntryNotFound -> EntryView {    var entry: Entry = attempt Entry.get(entry_id)    var finished: Entry = Entry.update(entry) {        set: {            finished_at: clock.now        }    }    return EntryView {        id: finished.id        title: finished.title        finished_at: finished.finished_at    }}

Make the data a test starts from

A fixture builds data for tests, in a domain's fixtures/ folder. Only tests call fixtures, and a fixture may write straight to an entity, which a test can't:

domains/reading/fixtures/a_reader.jadpo:

jadpo
use reading/entities { Reader }use reading/failures { ReaderExists }/// A reader with the address given.fixture a_reader(email: Reader.email) -> Reader {    return attempt Reader.create {        set: {            email: email        }        conflict: {            Reader.email: ReaderExists        }    }}

domains/reading/fixtures/an_entry.jadpo:

jadpo
use reading/entities { Entry, Reader }use reading/types { EntryTitle }/// A book on the reader's list, not yet read.fixture an_entry(reader: Reader, title: EntryTitle) -> Entry {    return Entry.create {        set: {            reader_id: reader.id            title: title            finished_at: none        }    }}

Test an action

An action test calls the action directly, as code does. clock: fixes the time the test runs at, so a value worked out from clock.now can be checked exactly:

domains/reading/actions/finish_entry.test.jadpo:

jadpo
use reading/entities { Entry, Reader }use reading/types { EntryView }use reading/fixtures { a_reader, an_entry }test "finishing a book records when" {    clock: "2099-03-01T09:30:00Z"    var ann: Reader = a_reader("ann@example.com")    var entry: Entry = an_entry(ann, "Middlemarch")    var finished: EntryView = attempt finish_entry(entry.id)    assert finished.finished_at == "2099-03-01T09:30:00Z"}
  • A test's name is a sentence saying what it proves.
  • attempt passes the action's failures on: a test that hits one fails, naming it.
  • assert takes any condition. assert finish_entry(id) fails EntryNotFound passes only when the call fails with that failure.

Test a route

A route test sends a request through the whole HTTP boundary: the input is decoded, the caller signed in, policy checked and the failure turned into a status, as for a real client. as: makes the request as someone, without going through sign-in:

domains/reading/routes/entries.test.jadpo:

jadpo
use reading/entities { Entry, Reader }use reading/types { NewEntry }use reading/fixtures { a_reader, an_entry }test "adding a book answers 201" {    var ann: Reader = a_reader("ann@example.com")    var response: Response = request {        method: HttpMethod.post        path: "/entries"        as: Principal.user(ann)        input: NewEntry {            title: "Middlemarch"        }    }    assert response.status == 201}test "the list needs someone signed in" {    var response: Response = request {        method: HttpMethod.get        path: "/entries"    }    assert response.status == 401    assert response.code == "authentication_required"}test "nobody finishes another reader's book" {    var ann: Reader = a_reader("ann@example.com")    var bo: Reader = a_reader("bo@example.com")    var entry: Entry = an_entry(ann, "Middlemarch")    var response: Response = request {        method: HttpMethod.post        path: "/entries/{entry_id}/finish"        params: {            entry_id: entry.id        }        as: Principal.user(bo)    }    assert response.status == 404    var listed: Response = request {        method: HttpMethod.get        path: "/entries"        as: Principal.user(bo)    }    assert listed.output == []}
  • request { } takes the method, the path with its params:, the caller in as:, and the input:. Leave out as: to call as nobody.
  • The Response has the status, the error's code, and the output.
  • The last test proves that policy keeps one reader's list from another: Bo gets a 404 for Ann's entry, as if it weren't there.

Run them

Run jadpo migrate new initial and jadpo api update if you haven't, then:

shell
jadpo test
output
tested: 4 passed, 1 action test, 3 route tests

Jadpo works out each test's kind from what it calls, and counts each kind apart, since an action test doesn't prove the route decodes its input or checks who is signed in. When a test fails, it shows each value in the assertion and the command that reruns just that test:

text
FAIL  finishing a book records when --> domains/reading/actions/finish_entry.test.jadpo:10  assert finished.finished_at == "2099-03-01T09:31:00Z"         |        |         |        "2099-03-01T09:30:00Z"         EntryView { id: "03b506f3-b9c0-71f9-8d76-e4864446acb7", title: "Middlemarch", finished_at: "2099-03-01T09:30:00Z" }  expected: "2099-03-01T09:31:00Z"  actual: "2099-03-01T09:30:00Z"  rule: test.assertion_failed  rerun: jadpo test domains/reading/actions/finish_entry.test.jadpo:10 --seed 1test failed: 1 failed, 3 passed, 1 action test, 3 route tests

To run fewer tests, name a file, jadpo test <file>, or a test, jadpo test <file>:<line>; or use --name <text>, --kind route, or --failed for those that failed last time. jadpo test --watch reruns the tests a change reaches each time you save.

Other things tests do

  • Calls to other services are never made: fakes: gives each call its answer (Call an outside API).
  • Emails are caught, and email.sent lists them (Add sign-in).
  • Schedules run with .run(), and the work they start finishes before the next line (Run background work).
  • clock.advance(1d) moves a test's clock on.

Read more

Where tests live, Writing a test, Kinds of test, Acting as a user, Controlling time, Shared test data: fixtures, Assertions, Running tests and The test database.

Markdown for agents: /guides/guide-write-tests.md.Built from Jadpo main at e00e61f. Jadpo's CI compiles this guide's code and runs its tests.