Testing
One Vitest preset shared by every package and app.
@kreogen/testing exports a Vitest configuration preset. A package that wants
tests writes three lines:
import { definePreset } from '@kreogen/testing';
export default definePreset({ root: import.meta.dirname });bun run test # everything, through turbo
bun test --filter authOptions
| Option | Default | For |
|---|---|---|
root | required | import.meta.dirname of the package |
environment | "node" | "jsdom" for anything rendering React |
react | implied by jsdom | Adds the React plugin |
setupFiles | [] | Resolved relative to root |
coverageThreshold | none | Minimum line and function coverage |
The threshold applies only when coverage is collected, so a plain vitest run
stays fast.
What the preset settles
The @kreogen/* alias. Packages resolve to source, and that alias has to
exist wherever code is resolved. It used to be written by hand in every vitest
config as well as in the tsconfig — four copies of one path, with a note in
CLAUDE.md asking people to remember all of them.
Two remain: packages/typescript-config/nextjs.json, which the editor and
tsc read, and this package, which every test run reads. A test in
packages/testing/__tests__ fails when they disagree — nothing else would
notice, and the symptom is a suite resolving a stale path and passing against
code nobody is shipping.
A server-only stub. That module throws outside a React Server Component
graph, and a unit test is not one. Stubbing it is what makes the server modules
— the ones that most need testing — importable at all.
Placeholder environment variables. Every package validates its environment
at import through keys(), which is right for a running app and wrong for a
test asserting on a pure function. DATABASE_URL, BETTER_AUTH_SECRET and the
public URLs get parseable defaults, applied only when genuinely unset — so a
suite running against real infrastructure still gets the real values.
DATABASE_URL's placeholder is a valid connection string rather than an
obviously fake one, because it is handed to a pg pool whose constructor
parses it eagerly. Nothing connects unless a test issues a query.
Machine-readable output in CI. JUnit reporter and coverage in
text-summary, lcov and cobertura, written under the package directory.
That placement matters: turbo runs tests per package, and the pipeline's
earlier globs pointed at repository-root paths nothing wrote — so every run
published an empty report and no coverage at all, while going green.
Why it calls defineConfig itself
definePreset returns a finished config rather than an object for the caller
to wrap. Wrapping was the obvious API and it does not typecheck: bun resolves
more than one copy of vite, and a config produced against one copy's types is
not assignable to another's defineConfig, even at identical versions. Doing
it here means only this package's copy is ever involved.
What is excluded from coverage
__tests__, generated, *.config.* and keys.ts. Generated Prisma output
and env schemas are not code anyone writes tests for, and leaving them in makes
the number meaningless.
The one suite that boots a server
apps/web/__tests__/routes.test.ts is the exception to everything above. It
starts a real Next server with every declared variable emptied and requests
every route, asserting that none answers 5xx, that an unknown post is a 404 and
that /sitemap.xml matches the pages on disk.
It exists because the property it checks cannot be checked structurally.
packages/cms degraded correctly in its query functions and threw from the
Feed component beside them, which put /legal/terms and /legal/privacy —
both linked from the sign-up form — on a 500 in any project generated with
cms and no token. A structural check was tried against both bugs in that
route and caught neither.
It costs roughly ninety seconds, almost all of it Turbopack compiling routes on first request. If that becomes too slow, shorten the route table rather than replacing the approach.
In CI
The test job runs against a real Postgres service. Most suites use
stand-ins; that database exists so tenancy and auth are checked against the
thing that actually enforces the constraints.