Testing
How tests are organised in zap.ts, how to give a test a real database, and why you cannot mock modules here.
zap.ts runs tests with Vitest. The end-to-end test uses Playwright instead.
Run everything:
pnpm run test
With a coverage report, which shows how much of your code the tests reach:
pnpm run test:coverage
Where tests live
A test file sits next to the file it tests. Not in a separate tests/ folder.
packages/billing/src/
├── access.ts
└── access.node.test.ts
You see the test when you open the folder. When you delete the code, you see the test to delete too.
The two kinds of test
The name of the file decides how it runs.
| File name | Runs in | Use it for |
|---|---|---|
*.node.test.ts |
Node | Logic, database work, anything with no screen |
*.browser.test.tsx |
A real Chromium browser | React components and hooks |
If your test needs a screen, a click or a DOM, it must be a browser test.
Name it .browser.test.tsx and it runs in a real browser, not a fake one.
Use vitest-browser-react to render components.
Giving a test a real database
Do not fake the database. Ask for a real one:
import { createTestDatabase } from "@zap-ts/testing/database";
const database = await createTestDatabase();
afterAll(() => database.close());
You get an empty database with all your tables already created. Your code then connects to it the same way it does in production.
Where that database runs depends on one setting:
- If
TEST_DATABASE_URLis set, it uses that Postgres server. - If it is not set, it starts PGlite instead. PGlite is a small Postgres that runs inside the test itself, with nothing to install.
So the tests work on a new computer with no setup. Set TEST_DATABASE_URL
when you want them to run against real Postgres:
export TEST_DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/postgres
You cannot mock modules
Mocking a module means replacing a whole file with a fake one at test time. zap.ts does not allow it. The linter blocks it.
Mocks drift. The fake keeps the old shape after the real code changes, so the test passes while the app is broken.
Do one of these two things instead.
Pass the dependency in. If a function needs a clock or a mailer, give it one as an argument. The test then passes a different one.
Write a small fake of the real thing. Build a tiny object with the same shape as the real interface. TypeScript then breaks your fake when the real interface changes, which is exactly what you want.
The end-to-end test
An end-to-end test opens a real browser and clicks through the app like a person would.
pnpm run test:e2e
This one does not use Vitest. It uses Playwright, it starts a real dev server, and it runs in three browsers: Chrome, Firefox and Safari.
It needs a real Postgres. Set TEST_DATABASE_URL before you run it, or it
stops and tells you so:
TEST_DATABASE_URL is required to run the end-to-end test
Use the same database as your other tests. The test pushes the schema itself before it starts.
How to write a test
Write the test first. Watch it fail. Then write the code that makes it pass.
A test that has never failed has never proved anything.
Related
- Local development — starting the database
- Conventions — the other rules of the codebase
- Database — tables and migrations
- Add a feature — a feature with its test