---
title: Testing
description: 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](https://vitest.dev). The end-to-end test uses
[Playwright](https://playwright.dev) instead.

Run everything:

```bash
pnpm run test
```

With a coverage report, which shows how much of your code the tests reach:

```bash
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:

```ts
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_URL` is 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:

```bash
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.

```bash
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](/guides/local-development) — starting the database
- [Conventions](/concepts/conventions) — the other rules of the codebase
- [Database](/packages/database) — tables and migrations
- [Add a feature](/recipes/add-a-feature) — a feature with its test
