Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: Testing

Testing

Fakes for integrations

On this page

A test never reaches a real provider, the outside service an integration talks to. Each provider is described to your app by a package (Integrations). Every provider call a test reaches answers through its package's fake, with the operation's generated default, unless the test's fakes: scripts it.

Scripting answers: fakes:

A script lists the responses an operation gives, in order. Each is a value of the operation's output type, or one of the provider conditions the integration maps, such as rate_limited (Calling). The response still goes through the integration's real code: input checks, retries, time limits and failure mapping. So a test proves how your code handles each outcome.

example
test "a rate-limited reminder is retried" {    fakes: {        ReminderMail.send_overdue_reminder: [rate_limited, ReminderReceipt { accepted_at: "2026-01-15T12:00:00Z" }]    }    ...    var sent: List<ReminderMessage> = ReminderMail.send_overdue_reminder.requests    assert list.count(sent) == 2}

Here the first send is rate-limited and the retry is accepted, so the code sent two requests.

What the code sent: .requests

Integration.operation.requests lists what the code sent, as typed values with secrets left out.

What can't be faked

A fake can't run code of its own, and nothing else in the application can be faked. A package's action runs its real code in a test, and fakes: scripts the operations it calls, its own package's included.

The number of answers

If the code calls an operation more times than the fake has responses, or fewer, the test fails and says so (test.fake_exhausted or test.fake_unused). A wrong count is a mistake in the test or the code, not a provider outcome.

An example

Here a courier's booking is faked three ways: with a scripted AcmeCouriers.Booking, with the operation's slot_taken, which the action maps to its own failure, and unscripted, with the generated default:

example
use app { couriers }use shipping/failures { SlotGone }use shared/providers/acme_courierstest "the courier's reference is kept" {    fakes: {        couriers.bookings.create: [            AcmeCouriers.Booking {                id: "b-17"            }        ]    }    var booking: AcmeCouriers.Booking = attempt book("N1 9GU")    assert booking.id == "b-17"}test "a taken slot is the app's own failure" {    fakes: {        couriers.bookings.create: [slot_taken]    }    assert book("N1 9GU") fails SlotGone}test "an unscripted call answers with the generated default" {    var booking: AcmeCouriers.Booking = attempt book("N1 9GU")    assert booking.id == "example"}

Values a fake answers with

A test scripts a value of the operation's output type, as Jadpo code sees it. The fake then encodes it as the provider would send it, with the package's wire_name: spellings and output_at: envelope, and reads it back through the operation's real decoding, as a real answer is. So a scripted answer exercises the package's encoders and decoders too.

So is a generated default. It is a value of the output type that keeps the type's rules:

  • text of its least length, example where that fits;
  • a number at its least;
  • an enum's first plain variant;
  • empty lists and maps;
  • none for what is optional;
  • the test's clock for an instant;
  • a fresh UUID.

A type whose rules no such value keeps, such as one with a pattern, faults where the answer is read, naming the field and the rule. The test then scripts that operation instead.

Provider calls and capabilities in tests

Every provider call in a test is faked, as in jadpo dev. Nothing can be let through to a provider's sandbox from a test.

Provider operations

A provider operation answers with its package fake's valid defaults unless the test scripts it: fakes: { stripe.payment_intents.create: [card_declined, ...] }. A fake's lost answers as a call that may have acted, and its in_use as unavailable. Each keeps keys as the next section says.

A scripted answer is a value of the clean output type or one of the operation's provider failures. The test never writes the wire shape: the fake encodes the value and decodes it, as "Values a fake answers with" says. jadpo calls script sets the same answers by hand in jadpo dev (Debugging and review).

Several changing calls

An action can make more than one outside call that changes something, such as creating a customer and then charging them. Jadpo keeps a record of its steps, and each call carries an idempotency key, so a rerun after a crash picks up where it stopped (Several changing calls in one action).

In a test, fakes honour those keys. A repeated step under the same key gets the stored answer. A script can also answer lost, meaning the request went and no answer came back, or in_use, meaning another runner holds the key.

jadpo test reruns every test that reaches such an action, once with a crash at each step boundary: before the send, after the send, and after the answer is recorded. This is on by default, and the test must pass every time. The report names the boundary of any run that fails.

A keyed call's answer is kept under its key:

  • The same key and request get the first answer again, without using up a scripted one.
  • A different request under the same key gets the package's reused: answer.
  • A failure is kept as the provider keeps it, unless its kind is unavailable or rate_limited.
  • lost acted, so what it did, the generated default, answers the next call under its key.
  • in_use, unavailable and misconfigured did nothing, and keep nothing.

A test that passes, and reached steps under a record, runs again once for each boundary of each step it reached. Each run uses its own copy of the database and a fresh fake. The crash stops the run with nothing it wrote kept. The delivery is given back and runs again at once from its record, and the crash doesn't count as an attempt. A route's request is sent again under its key.

A delivery stops as diverged when a rerun doesn't match the steps its record holds. One that stops as diverged between a test's steps ends the test as test.subscriber_failed. A crash's failure begins with a crash line, also in --json:

text
FAIL  a chosen plan starts one subscription --> domains/billing/actions/choose.test.jadpo:3  crash: before step 1 of `subscribe`, `pay.customers.create`, went out; then it ran again  delivery: `PlanChosen` to subscriber `start_subscription` stopped as diverged on attempt 1 of 10

If a crash leaves the fake's effects different from the run without one, for example a call that acted under a new key, that is a fault in Jadpo, test.fault.

A crash sends its step again. The ai fake, or a call with no key, answers that with the next scripted answer, as a model or provider asked again would answer afresh. So test.fake_exhausted and test.fake_unused are judged on the run without a crash only.

A request or delivery that a crash sends again keeps none of the ai answers the crashed run was given, as a restarted runner keeps none. So a model asked before the steps is asked again, and a different answer diverges there, as it would in production. Why it works this way is in the Jobs and events design notes.

One fake per capability

A capability is a service Jadpo provides itself, such as email, which a package can fill (Email). email.sent lists the mail the code sent. There is no sms.sent, since SMS is a package whose calls are faked like any other. A package that fills a capability isn't faked separately: the capability's fake stands in for every connection that fills it.

Documents

documents.pdf returns a placeholder PDF, and documents.html the real rendered HTML, which a test can assert on (Documents).

Bot checks

Bot checks pass through a local driver. A test scripts a failure with fakes: { bot_check: [bot_check_failed] }, and the request is refused with 403 bot_check_failed before the action runs.

Search uses PostgreSQL's full-text search, whatever engine production names (Search).

OAuth grants

An OAuth grant lets the app call another service on a user's behalf (Acting for each user: OAuth grants). A test declares its grants at the top, beside fakes:, as grants: { calendar: [alice] }: the connection's name, and the fixtures whose records own a grant, as jadpo dev grants add makes them in development. A call on behalf of an owner with no grant fails grant_missing, and a script can answer grant_revoked.

Files

In a test, an in-memory store takes the place of every files connection (Files). It can be told to fail a read (unavailable) or a write (storage_full). A real file sits in a domain's fixtures/ folder, such as fixtures/files/customers.csv, and a test turns it into a stored File with fixture_file("customers.csv", store: imports).

Batches

A batch run works through many items, such as the rows of a file, a few at a time (Batches: work over many items). In a test, batches run to the end within the test, after the action that started them commits, like events. jadpo test runs them in batches of two items and simulates a restart between every batch, so every import or export test also tests resuming. A test asserts on what the app recorded and on the summary finished: received, and can read batches.status.

Not built yet

These parts of fakes aren't built yet:

  • .requests, apart from the ai fake's ai.<prompt>.requests.
  • The capabilities' fakes, apart from email.sent and the ai capability's: its fake, fakes: { ai.<prompt>: }, ai.<prompt>.requests, recorded("<name>"), the fake's embeddings and fakes: { ai.embed: } (Testing AI calls).
  • fixture_file, and telling the file store to fail from a test. The in-memory store itself is built.
  • Running batches to the end: a test sees a run as it starts, and a file's run reads two rows a batch on a test's host.

What is built is every call a test reaches answering through its package's fake, and a test's fakes: scripting an operation's answers in order, each a value of its output type, one of its failures by its code, or a condition. Why fakes work this way is in the Integrations design notes.

Markdown for agents: /docs/fakes-for-integrations.md.Built from Jadpo main at a707efb.