spec runners
Spec runners enable you to run either unit or feature specs. These are written directly to your api/package.json file (or your package.json file if your app is api-only), enabling you to customize them to your liking:
# run all unit specs
pnpm uspec
# run all feature specs (headless)
pnpm fspec
# run feature specs in a visible browser
pnpm fspec:visible
Running a single file or path
Both runners accept one or more path arguments, which are passed straight through to vitest:
# a single file
pnpm uspec spec/unit/models/Place.spec.ts
# multiple patterns in a single invocation
pnpm fspec spec/features/host spec/features/guest
It's fine to pass multiple file patterns to a single uspec or fspec invocation — the runner manages that suite's lifecycle internally.
vitest's positional path filters match by prefix/substring against file paths, not by real path-scoping. {{PM}} fspec spec/features/host also runs a sibling directory like spec/features/host-verification/, because host is a prefix match on both paths. Pass a full file path when you need an exact single-file run.
Running specific specs within a file
Use .only or .skip on it, describe, or context blocks to narrow or exclude a run without changing the command line:
it.only('returns the index of Places', async () => { ... }) // Run only this
it.skip('returns the index of Places', async () => { ... }) // Skip this
// Also works on describe and context blocks
.only and .skip are meant for narrowing a local run, so take care not to commit them.
Prerequisites
Run migrations before specs. After generating a resource or migration, run {{PM}} psy db:migrate before {{PM}} uspec or {{PM}} fspec. Without it, the integrity check fails with "Migrations need to be run on default database".
Both suites truncate the same test database between runs, so running them at once produces truncation and setup/teardown races.
The constraint is the database, not the machine. Once each suite has a database of its own, they can run at the same time, which is exactly what you want in CI.
Feature specs manage their own frontend servers. fspec automatically starts and stops the frontend servers (client, admin, internal) it needs. Don't start them manually before running fspec — a server already bound to the port will make fspec fail when it tries to bind the same port itself.
Running specs in parallel in CI
Since a spec run is mostly waiting on the database, running the suites in parallel is usually the difference between a CI run you wait on and one you don't. There are two independent ways to get that parallelism, and they compose.
The first is to give each suite its own database by running it as its own CI job. The workflow generated by create-psychic does this: uspec and fspec are separate jobs, each on its own runner with its own Postgres service container, so the two suites never share a database and the warning above doesn't apply. In GitHub Actions that looks like:
jobs:
uspec:
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: psychic
POSTGRES_DB: bearbnb_test
POSTGRES_PASSWORD: 'postgres'
steps:
- run: pnpm psy db:migrate --skip-sync
- run: pnpm uspec
fspec:
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: psychic
POSTGRES_DB: bearbnb_test
POSTGRES_PASSWORD: 'postgres'
steps:
- run: pnpm psy db:migrate --skip-sync
- run: pnpm fspec
The second is to partition a single suite across several databases, so that one uspec run can execute more than one spec file at a time. This is what parallelTests is for, and the generated workflow sets it through an environment variable:
env:
DREAM_PARALLEL_TESTS: '2'
Dream then makes that many copies of the database and hands each concurrent test run a database of its own, so the truncation races the warning describes can't happen.
Beyond that, vitest can shard the spec files themselves across runners, with each shard getting its own runner and therefore its own database. A new app has too few spec files for this to be worth it, but as a suite grows it's the next thing to reach for:
strategy:
matrix:
shard: ['1/2', '2/2'] # keep the denominators equal
steps:
- run: pnpm uspec --shard=${{ matrix.shard }}