sdxc
SearchCtrlK

[ 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 make writes 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 filebun run test app/models/bookmark.test.ts
every file whose path matchesbun run test bookmarks
the cases whose name matchesbun run test app/http/controllers/bookmarks -t "guest"
the case on one linebun run test app/models/bookmark.test.ts:29
again on every savebunx 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.

KindThe generated testYou add
controllerthe module exports what the router maps, and a test.todowhat the page answers
validatora complete submission passes, and one missing a field failseach rule you add to the schema
modela row the factory creates reads backeach method and callback
policywho may read, create, update and deleteeach condition you add to a grant
job, MCP toolthe handler is exported where it is mapped, and a test.todothe work
commandthe command runs through the app's CLI and exits 0, and a test.todoits output and effects
middlewarethe factory returns a middlewarewhat it publishes or answers
servicethe function succeedseach outcome its callers branch on
emailits preview props render into the subject and both body partsthe copy your app sends
initializerthe instance is built at importhow 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.ts has migrate(), which applies every migration in database/migrations/ to the in-memory DB binding, and factories(), which writes rows through your own models. Call migrate once per file in beforeAll; each file starts with an empty database.

  • app/test/session.ts has signIn(actor?), which answers the session cookie of a signed-in member, for a request that needs one.

  • app/test/cloudflare-workers.ts stands in for cloudflare:workers under Vitest, so a module that imports env loads in a test as it does in the Worker. Its EMAIL binding keeps every message sent through it on messages.

  • app/test/local-database.ts has useTemporaryProject(migrations), which points the command line at a temporary project, so a command's test runs db migrate and db seed without touching your local database.

A page test builds the router the Worker builds and sends it a Request:

app/http/controllers/bookmarks/index.test.tsx
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() and Model.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 with testAccess(policy, { roles, facts }), as the policy's test does, without a request.

  • A schema's rule is tested by validate(form, Schema) on a URLSearchParams.

  • An email's copy is tested by mail.render(emails.teams.invite, { props }) from app/initializers/mail.ts, as the email's own test does; a page that sends one checks env.EMAIL's messages, and app/initializers/mail.test.ts fails 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.

Written by Sergio Xalambrí. Follow @sergiodxa for new packages, or sponsor the work.