Testing
Built-in Fixtures
Pre-built fixtures for PostgreSQL and Prisma
The testing package includes ready-to-use fixtures for common infrastructure.
PostgreSQL Container
Spins up a PostgreSQL container using Testcontainers:
import { buildHarness, postgresContainer } from "@lawhive/framework-testing"
const ctx = buildHarness()
.use(() => ({
pg: postgresContainer({
image: "postgres:16-alpine",
database: "test",
username: "test",
password: "test",
}),
}))
.setup()
// Access connection string
console.log(ctx.pg.connectionString)
// postgres://test:test@localhost:32768/testOptions
| Option | Type | Default | Description |
|---|---|---|---|
image | string | "postgres:16-alpine" | Docker image |
database | string | "test" | Database name |
username | string | "test" | Username |
password | string | "test" | Password |
Return Value
type PostgresContainer = {
connectionString: string
}Container Lifecycle
The container:
- Starts in
beforeAllwith wait strategy for "ready to accept connections" - Provides a unique port mapped to 5432
- Stops in
afterAll
Bootstrap Prisma
Pushes a Prisma schema and returns a connected client:
import { buildHarness, postgresContainer, bootstrapPrisma } from "@lawhive/framework-testing"
import { PrismaClient } from "./generated/client"
const ctx = buildHarness()
.use(() => ({
pg: postgresContainer(),
}))
.use(({ pg }) => ({
prisma: bootstrapPrisma({
client: PrismaClient,
schemaPath: "./prisma/schema.prisma",
connectionString: pg.connectionString,
}),
}))
.setup()Options
| Option | Type | Required | Description |
|---|---|---|---|
client | PrismaClientClass | Yes | The PrismaClient class (not instance) |
schemaPath | string | Yes | Path to schema.prisma file |
connectionString | string | Yes | Database connection URL |
prismaBin | string | No | Path to Prisma CLI binary |
How It Works
- Runs
prisma db push --schema=<path>with the connection string - Creates a new
PrismaClientinstance withdatasourceUrl - Disconnects in cleanup
Prisma Binary Resolution
If prismaBin is not provided, it searches:
- Walk up from
schemaPathlooking fornode_modules/.bin/prisma - Falls back to
npx prisma
// Explicit binary path
bootstrapPrisma({
client: PrismaClient,
schemaPath: "./prisma/schema.prisma",
connectionString: pg.connectionString,
prismaBin: "./node_modules/.bin/prisma",
})Creating Custom Fixtures
Basic Pattern
import type { FixtureFactory } from "@lawhive/framework-testing"
type MyService = {
doSomething: () => Promise<void>
}
export const myServiceFixture = (config: {
apiKey: string
}): FixtureFactory<MyService> => ({
create: async () => {
const service = new MyService(config.apiKey)
await service.connect()
return service
},
cleanup: async (service) => {
await service.disconnect()
},
})
// Usage
const ctx = buildHarness()
.use(() => ({
service: myServiceFixture({ apiKey: "test-key" }),
}))
.setup()With Dependencies
export const seededDatabase = (opts: {
prisma: PrismaClient
}): FixtureFactory<{ users: User[] }> => ({
create: async () => {
const users = await opts.prisma.user.createMany({
data: [
{ email: "alice@example.com" },
{ email: "bob@example.com" },
],
})
return { users }
},
cleanup: async () => {
await opts.prisma.user.deleteMany()
},
})
// Usage
const ctx = buildHarness()
.use(() => ({ pg: postgresContainer() }))
.use(({ pg }) => ({
prisma: bootstrapPrisma({ /* ... */ }),
}))
.use(({ prisma }) => ({
seed: seededDatabase({ prisma }),
}))
.setup()Redis Container Example
Create a Redis fixture following the same pattern:
import { GenericContainer, type StartedTestContainer } from "testcontainers"
import type { FixtureFactory } from "@lawhive/framework-testing"
type RedisContainer = {
host: string
port: number
url: string
}
type RedisContainerInternal = RedisContainer & {
container: StartedTestContainer
}
export const redisContainer = (opts?: {
image?: string
}): FixtureFactory<RedisContainer> => ({
create: async () => {
const container = await new GenericContainer(opts?.image ?? "redis:7-alpine")
.withExposedPorts(6379)
.start()
const host = container.getHost()
const port = container.getMappedPort(6379)
return {
host,
port,
url: `redis://${host}:${port}`,
container,
} as RedisContainerInternal
},
cleanup: async (redis) => {
const internal = redis as RedisContainerInternal
await internal.container.stop()
},
})Tips
Parallel Test Files
Each test file that calls .setup() gets its own container:
tests/
├── user.test.ts # Own PostgreSQL container
├── task.test.ts # Own PostgreSQL container
└── auth.test.ts # Own PostgreSQL containerThis provides isolation but uses more resources. For faster CI, consider:
- Sharing containers across test files (export/import harness)
- Running test files serially with
--no-threads
Debug Containers
Keep containers running after tests:
const ctx = buildHarness()
.use(() => ({
pg: {
...postgresContainer(),
cleanup: async () => {
// Don't stop - leave running for debugging
console.log("Container still running for debugging")
},
},
}))
.setup()Environment Variables
Use the connection string in environment:
const ctx = buildHarness()
.use(() => ({ pg: postgresContainer() }))
.use(({ pg }) => ({
env: {
create: () => {
process.env.DATABASE_URL = pg.connectionString
return { originalUrl: process.env.DATABASE_URL }
},
cleanup: ({ originalUrl }) => {
process.env.DATABASE_URL = originalUrl
},
},
}))
.setup()