Testing with kick/db
Three things make database tests pleasant: a database that's fast to create, a clean slate for every test, and a way to hand that database to the code under test. @forinda/kickjs-db/testing gives you all three.
A database per test file
For unit and service tests, createTestDb builds an in-memory SQLite database from your schema in milliseconds, with foreign keys enforced:
// src/users/users.service.test.ts
import { afterAll, beforeAll, it, expect } from 'vitest'
import { createTestDb } from '@forinda/kickjs-db/testing'
import * as schema from '../db/schema'
let db: Awaited<ReturnType<typeof createTestDb<typeof schema>>>
beforeAll(async () => (db = await createTestDb({ schema })))
afterAll(() => db.destroy())Each call is a separate database, so test files never see each other's rows. It needs better-sqlite3 installed. This works for any schema SQLite can express; Postgres-only column types (tsvector, vector, enums, …) need a real Postgres — see below.
If your app runs on SQLite, apply your actual migrations instead, so tests run the same SQL production does:
db = await createTestDb({ schema, migrationsDir: 'db/migrations' })Unreviewed migrations apply here — tests aren't deploys.
Roll back after every test
rolledBack runs a test inside a transaction that never commits:
import { rolledBack } from '@forinda/kickjs-db/testing'
it('creates a user', () =>
rolledBack(db, async () => {
const user = await service.create('a@b.c')
expect(user.email).toBe('a@b.c')
}))Because transactions follow the call chain, code that only holds the injected client — a service, a repository — runs inside the test's transaction without being handed tx, and everything it wrote disappears afterwards. A failing assertion still fails the test. Two things to know:
afterCommithooks never run in a rolled-back test — assert on what was queued, or test the hook on its own.- A
transaction({ nested: 'separate' })inside the code under test commits on its own connection (and isn't possible on SQLite at all).
Hand the database to your app
createTestApp replaces any binding through overrides. Swap the token your app registers the client under:
import { createTestApp } from '@forinda/kickjs-testing'
import request from 'supertest'
import { APP_DB } from '../src/db/token'
const db = createTestDb()
const { app } = await createTestApp({
modules: [UsersModule()],
overrides: [[APP_DB, db]],
})
const res = await request(app.handle.bind(app)).get('/api/v1/users')Use the entries form ([[TOKEN, value]]) — see Replace a dependency.
Against real Postgres
Integration tests that need Postgres behaviour — Postgres types, serializable conflicts, row-level security — need a Postgres server: a CI service container, a local Docker Postgres, or Testcontainers. Point createPgTestDb at it and each test file gets its own throwaway database on that server, with your schema or migrations applied — well under a second, no container per file:
import { afterAll, beforeAll } from 'vitest'
import { createPgTestDb, type PgTestDb } from '@forinda/kickjs-db/testing'
import * as schema from '../src/db/schema'
let pg: PgTestDb<typeof schema>
beforeAll(async () => {
pg = await createPgTestDb({
schema, // or migrationsDir: 'db/migrations'
connectionString: process.env.TEST_DATABASE_URL!, // a database you may CREATE DATABASE from
})
})
afterAll(() => pg.drop()) // closes the client and drops the database
it('…', () =>
rolledBack(pg.db, async () => {
/* … */
}))pg.connectionString points at the throwaway database, for code that opens its own pool. It needs pg installed. In GitHub Actions, a services: postgres: entry gives you the server — see CI and Deployment.
Asserting database errors
Failures are typed errors, so tests assert the kind, not a driver message:
import { UniqueViolationError } from '@forinda/kickjs-db'
await expect(service.create('taken@b.c')).rejects.toBeInstanceOf(UniqueViolationError)
await expect(service.create('taken@b.c')).rejects.toMatchObject({ columns: ['email'] })Related
- Testing —
createTestApp, modules, environment isolation - Queries → Transactions
- Errors