[ Operations & testing ]
Test your app
Read the tests sdxc make and sdxc scaffold write beside each file, run one file or one case while you work, and turn a generated test into one that pins your app's behavior.
Last updated 2026-10-19
After reading this guide, you will know
- What each test
sdxc makewrites checks, and what is left for you - How to run every test, one file, one case or one line
- How the test helpers in
app/test/give a test the app's real router and database
Every file sdxc make and sdxc scaffold write comes with a test beside it, so a new app
starts with a passing suite and every feature you add has a place for its cases. This guide
starts from those tests. Test Workers apps covers the
configuration under them: the Workers pool, the in-memory mocks and MSW.
Run the tests
bun run test
bun run test runs the project's test script, vp test run: Vitest
through Vite+, over every *.test.ts and *.test.tsx under app/ and resources/. Anything
after the script name reaches Vitest, so you narrow a run the way Vitest does.
| To run… | Type |
|---|---|
| one file | bun run test app/models/bookmark.test.ts |
| every file whose path matches | bun run test bookmarks |
| the cases whose name matches | bun run test app/http/controllers/bookmarks -t "guest" |
| the case on one line | bun run test app/models/bookmark.test.ts:29 |
| again on every save | bunx vp test watch app/models |
A path is matched as a substring, so bun run test bookmarks runs the model's, the validator's,
the policy's and every page's tests for the resource at once. The line number is the one the case's
test( call starts on; any other line answers "No test found".
What each kind's test checks
make writes the test that proves the file loads and is wired in, and leaves the cases about
your rules to you. That keeps a fresh suite green, and Vitest lists each test.todo as a gap to
fill.
| Kind | The generated test | You add |
|---|---|---|
| controller | the module exports what the router maps, and a test.todo | what the page answers |
| validator | a complete submission passes, and one missing a field fails | each rule you add to the schema |
| model | a row the factory creates reads back | each method and callback |
| policy | who may read, create, update and delete | each condition you add to a grant |
| job, MCP tool | the handler is exported where it is mapped, and a test.todo | the work |
| command | the command runs through the app's CLI and exits 0, and a test.todo | its output and effects |
| middleware | the factory returns a middleware | what it publishes or answers |
| service | the function succeeds | each outcome its callers branch on |
| its preview props render into the subject and both body parts | the copy your app sends | |
| initializer | the instance is built at import | how it is configured |
sdxc scaffold writes fuller page tests: they create rows with the factory, sign a member in,
and submit the forms with valid honeypot fields, so the page, the policy and the validator all
run as in the Worker. Use them as the model for a controller's own cases.
The helpers in app/test
The template and the scaffold put what every test needs in app/test/, so a test reads like the
code it checks:
app/test/database.tshasmigrate(), which applies every migration indatabase/migrations/to the in-memoryDBbinding, andfactories(), which writes rows through your own models. Callmigrateonce per file inbeforeAll; each file starts with an empty database.app/test/session.tshassignIn(actor?), which answers the session cookie of a signed-in member, for a request that needs one.app/test/cloudflare-workers.tsstands in forcloudflare:workersunder Vitest, so a module that importsenvloads in a test as it does in the Worker. ItsEMAILbinding keeps every message sent through it onmessages.app/test/local-database.tshasuseTemporaryProject(migrations), which points the command line at a temporary project, so a command's test runsdb migrateanddb seedwithout touching your local database.
A page test builds the router the Worker builds and sends it a Request:
beforeAll(migrate);
test("lists each bookmark, linking to its page", async () => {
let row = await factories().create(BookmarkFactory);
let response = await createWebRouter().fetch(
new URL(routes.bookmarks.index.href(), "https://example.com"),
);
expect(response.status).toBe(200);
expect(await response.text()).toContain(
`href="${routes.bookmarks.show.href({ id: row.id })}"`,
);
});
Every middleware in the chain runs, the database included, so the test catches a route that is
declared but not mapped, a policy that refuses the visitor, and a view that fails to render. A
link built with the contract's href() keeps the test passing when you change the URL pattern.
Test a rule where it lives
Put each case beside the code that holds the rule, and test it there with the least set-up:
A rule about one model is a method on the model; test it with
factories()andModel.bind({ db: openDatabase() }), as the model's own test does.A job's work is usually that method, or a service; test it there and keep the job's test to the mapping.
A rule about who may do something is a grant in
app/policies/policy.ts; test it withtestAccess(policy, { roles, facts }), as the policy's test does, without a request.A schema's rule is tested by
validate(form, Schema)on aURLSearchParams.An email's copy is tested by
mail.render(emails.teams.invite, { props })fromapp/initializers/mail.ts, as the email's own test does; a page that sends one checksenv.EMAIL'smessages, andapp/initializers/mail.test.tsfails on a declared email with no template mapped.
The page test then needs one case per outcome a visitor sees, rather than one per rule.
Factories
sdxc make model writes a factory beside the model, in database/factories/, with a value per
field that carries the factory's sequence, so two rows a test creates never collide on a unique
column. Override only the field a case is about:
let old = await factories().create(BookmarkFactory, { title: "Old link" });
Generate believable test data covers traits, seeds and the sample generators, and Make a factory what the command writes.
Before you commit
bun run test
bun check
bun check type checks the test files too, so a case that calls a renamed method fails there
even when it is skipped.
Where to go next
Test Workers apps — real bindings in the Workers pool, MSW for outbound HTTP, and mail and jobs recorded in memory.
Generate files — the flags that write a test, or skip one.