Testing patterns for the fullstack stack
For projects scaffolded with nextjs-fullstack-starter. Tests are first-class — the verification gate (pnpm verify) runs them on every PR.
Use this skill when
- Writing tests for a new service, action, or route handler.
- Adding coverage to existing code.
- Debugging a failing test.
- Deciding what to test at which layer.
- Reviewing a PR's test coverage.
The shape of testing in this stack
The architecture has four delivery shapes — Server Components, Server Actions, route handlers, MCP tools — all calling services. Most of your testing effort goes into the service layer. Delivery wrappers are thin enough that integration tests cover them implicitly.
Roughly:
| Layer | Test type | Why |
|---|---|---|
| Service | Unit tests with mocked Prisma | Where the business logic lives. Highest leverage. |
| Service (occasional) | Integration tests against real Postgres | When Prisma's query builder behaviors matter — joins, transactions, indexes. |
| Server Action | Direct invocation tests OR e2e | Thin wrapper — usually covered by e2e. Unit-test only when the action does non-trivial parsing. |
| Route handler | Direct invocation tests | Same as actions — thin. Test webhook signature verification and edge cases. |
| Page / UI | Playwright e2e | Server Components are server-rendered HTML; integration testing them is e2e. |
| MCP tool | Same as routes/actions — thin wrapper test | Verifies the tool registry contract; service logic is covered by service tests. |
Reference index
Read the file matching what you're testing:
| Testing this | Read this |
|---|---|
| A service method | references/service-tests.md |
| A Server Action | references/server-action-tests.md |
| A route handler / webhook | references/route-handler-tests.md |
| A page or user flow end-to-end | references/e2e-playwright.md |
Quick rules
- Mock at the boundary, not the layer above. When unit-testing a service, mock
@/server/db/clientand@/server/auth/permissions. Don't mock other services it calls — let those use their real (also-mocked-Prisma) implementations. - Test inputs and outputs, not internals. Don't assert on what private helpers got called. Assert on what came back and what the DB was asked to do.
- Use real
db.$transactionsemantics. When mocking Prisma, the$transactionmock should pass atxshaped like the same client. Otherwise audit-inside-transaction patterns appear broken in tests. - Permission denials get their own tests.
requirePermissionis critical — every service should have at least one test that confirms the denial path throws. - Don't test types. TypeScript runs ahead of every test; type-only invariants are already checked.
- Don't test the framework. No test for
requireSessionredirecting — that's Better Auth's job. Trust the upstream.
Running tests
pnpm test # one-shot
pnpm test:watch # watch mode
pnpm test -- customer # filter by name
pnpm test -- --updateSnapshot
The verification gate
pnpm verify
Chains format:check, tsc --noEmit, lint, build, test, db:diff. CI runs the same — the gate is canonical. If tests are flaky, that's a test bug, not a CI bug. Fix it; don't retry.