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:
a calculation first, then 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 /, GET / and POST /.
Add sign-in and Query records show how to
write those. The first test needs none of it.
Test a calculation
The smallest test checks an action that only works something out, so it touches no
stored record and no route. An action takes and returns named types. PageCount and
LongRead are this app's names for what its values mean, and Int and Bool are
built into Jadpo.
domains/:
/// How many pages a book has.type PageCount = Int { min: 1}
domains/:
/// Whether a book takes a while to read.type LongRead = Bool {}
domains/:
use reading/types { LongRead, PageCount }/// Whether a book of `pages` pages is a long read.action is_long_read(pages: PageCount) -> LongRead { return LongRead(pages > 500)}
A test sits beside what it tests, in a file ending .test.jadpo, and calls it without
importing it.
domains/:
test "a book over 500 pages is a long read" { assert is_long_read(800)}
- A test's name is a sentence saying what it proves.
asserttakes any condition.
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/:
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/:
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
The action to test marks a book on the list as read:
domains/:
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 }}
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/:
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"}
attemptpasses the action's failures on: a test that hits one fails, naming it.assert finish_entry(id) fails EntryNotFoundpasses 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/:
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 itsparams:, the caller inas:, and theinput:. Leave outas:to call as nobody.- The
Responsehas thestatus, the error'scode, and theoutput. - The last test proves that policy keeps one reader's list from
another: Bo gets a
404for 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:
jadpo testtested: 5 passed, 2 action tests, 3 route testsJadpo 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:
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, 4 passed, 2 action tests, 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.sentlists 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 aee2e8f. Jadpo's CI compiles this guide's code and runs its tests.