Testing
Two test suites, two commands:
- pnpm test — unit tests across every package. Pure CPU, no Postgres needed.
- pnpm test:integration — DB-backed tests for
@byline/clientand@byline/db-postgres. Runs against a dedicatedbyline_testPostgres database — neverbyline_dev.
CI runs both, in the same job, against a Postgres service container.
TL;DR
# One-time per machinecp packages/db-postgres/.env.example packages/db-postgres/.env # dev DBcp packages/db-postgres/.env.test.example packages/db-postgres/.env.test # test DBcp packages/client/.env.test.example packages/client/.env.test # client integration testscd postgres && ./postgres.sh up -d # start the containerpnpm db:init # create byline_dev (one-time)pnpm db:init:test # create byline_test (one-time)
# Every test runpnpm test # unit suites — no DBpnpm test:integration # integration suites — requires byline_testThe integration runner auto-migrates byline_test on startup (Drizzle's migrator is idempotent) and truncates every public table between test files. A crashed prior run can't leak state into the next.
What runs where
Package |
|
|
| ✅ vitest | — |
| ✅ vitest | — |
| ✅ vitest | — |
| ✅ vitest | — |
| ✅ vitest | — |
| ✅ vitest | — |
| ✅ vitest | ✅ vitest |
| ❌ no-op (every test needs a DB) | ✅ vitest |
Only @byline/client and @byline/db-postgres write to byline_test. Everything else is pure in-memory.
pnpm test (root) runs turbo run test. pnpm test:integration (root) runs turbo run test:integration --concurrency=1 — the concurrency flag serialises the two DB-backed suites so each one's per-file TRUNCATE doesn't wipe the other's seeded fixtures mid-run.
Two databases, two purposes
Database | Used by | Lifecycle |
|
| Created once, lives as long as you want, manual seed |
|
| Created once, wiped by the test runner between test files |
Both live in the same local Postgres container (postgres/docker-compose.yml). Same byline role. The split is logical, not physical — local Postgres is a dev tool.
Safety guards
Two layers prevent any test from ever pointing at the wrong database:
- Script-level (braces) —
packages/db-postgres/src/database/common.shparsesBYLINE_DB_POSTGRES_CONNECTION_STRINGand refuses to continue unless the derived database name ends in_devor_test.db_init.shanddb_init_test.shboth go through it. - Runtime (belt) —
assertTestDatabase()inpackages/db-postgres/src/lib/test-db.tsparses the connection string at the top of every test bootstrap and throws unless the DB name ends in_test. Imported by both the vitest globalSetup (packages/client/tests/_global-setup.ts) and the node:test bootstrap (packages/db-postgres/src/lib/test-bootstrap.ts).
Isolation strategy
- Migrate once per test run — vitest
globalSetupmigrates before any test file loads. Drizzle's migrator is idempotent so re-runs are cheap. - TRUNCATE between files —
setupFilestruncates every table inpublic(except__drizzle_migrations) withRESTART IDENTITY CASCADEvia abeforeAllat the top of each test file. Existing per-test track-and-clean code (e.g. the admin tests) stays in place as a belt; TRUNCATE is the braces. - No transaction-per-test — the storage code opens its own transactions; wrapping tests in one would break the lifecycle paths under test.
Both @byline/client and @byline/db-postgres use the same vitest config shape (globalSetup + setupFiles + fileParallelism: false + single-fork pool), so the isolation story is identical across packages.
CI
.github/workflows/ci.yml runs on every pull request and on direct pushes to develop / main. Two jobs:
- lint-and-typecheck —
pnpm install --frozen-lockfile→pnpm lint→pnpm typecheck. - test-suite — boots a Postgres service container with
byline_testpre-created, writes.env.testfiles from the job-level env block, then runspnpm test(unit) followed bypnpm test:integration. Both run in the same job so they share onepnpm install.
Both jobs skip when the head commit starts with chore(release): so version-bump pushes from pnpm version-packages don't trigger redundant runs. Tag pushes (git push --tags) and gh release create aren't listened to at all, so the local-only release flow stays silent.
concurrency: cancel-in-progress cancels superseded runs on the same branch — quick fix-up pushes don't queue behind older builds.
When branch protection is enabled in repo settings, CI becomes a hard gate with no workflow change required.
Running a single test
Both packages use vitest, so the invocation is the same shape:
# @byline/clientcd packages/client && pnpm vitest run --mode=integration tests/integration/client-read.integration.test.ts
# @byline/db-postgrescd packages/db-postgres && pnpm vitest run --mode=integration src/modules/storage/tests/storage-versioning.test.tsFilter by test name with -t:
pnpm vitest run --mode=integration -t "tampered"Watch mode (re-runs on file change):
pnpm test:watch