Skip to main content

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.

Path filters match by prefix, not by directory

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".

Run uspec and fspec one at a time locally

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 }}