Testing
Test Harness
Composable fixture builder for integration tests
The test harness provides a fluent API for composing test fixtures with automatic lifecycle management.
Basic Usage
import { buildHarness } from "@lawhive/framework-testing"
const ctx = buildHarness()
.use(() => ({
config: { apiUrl: "http://localhost:3000" },
}))
.use(({ config }) => ({
client: createApiClient(config.apiUrl),
}))
.setup()
describe("API", () => {
it("works", async () => {
const result = await ctx.client.get("/health")
expect(result.status).toBe("ok")
})
})The .use() Method
Each .use() call adds fixtures to the context:
const ctx = buildHarness()
// First layer: no dependencies
.use(() => ({
database: postgresContainer(),
redis: redisContainer(),
}))
// Second layer: can access first layer
.use(({ database }) => ({
prisma: bootstrapPrisma({
client: PrismaClient,
schemaPath: "./prisma/schema.prisma",
connectionString: database.connectionString,
}),
}))
// Third layer: can access all previous
.use(({ prisma, redis }) => ({
cache: createCache({ prisma, redis }),
}))
.setup()Fixture Factory Interface
Each fixture implements FixtureFactory<T>:
type FixtureFactory<TValue> = {
create: (deps: Record<string, unknown>) => Promise<TValue> | TValue
cleanup?: (value: TValue) => Promise<void> | void
}Simple Fixture
.use(() => ({
config: {
create: () => ({ port: 3000, host: "localhost" }),
// No cleanup needed
},
}))Async Fixture with Cleanup
.use(() => ({
server: {
create: async () => {
const server = createServer()
await server.listen(3000)
return server
},
cleanup: async (server) => {
await server.close()
},
},
}))The .setup() Method
Call .setup() to register beforeAll/afterAll hooks:
// Module level - shared across all tests in file
const ctx = buildHarness()
.use(() => ({ /* ... */ }))
.setup()
describe("tests", () => {
it("can access ctx", () => {
expect(ctx.prisma).toBeDefined()
})
})Scoped Setup
Call .setup() inside a describe block for scoped fixtures:
describe("isolated tests", () => {
const ctx = buildHarness()
.use(() => ({ /* ... */ }))
.setup() // Fixtures scoped to this describe
it("works", () => {
expect(ctx.database).toBeDefined()
})
})
describe("other tests", () => {
// Different fixtures, different database
const ctx = buildHarness()
.use(() => ({ /* ... */ }))
.setup()
})Guarded Context
The context proxy throws helpful errors if accessed too early:
const ctx = buildHarness()
.use(() => ({ value: { create: () => 42 } }))
.setup()
// ❌ Error: Cannot access fixture "value" before test setup completes
const x = ctx.value // At describe registration time
describe("tests", () => {
// ✅ Works - inside test callback
it("works", () => {
expect(ctx.value).toBe(42)
})
})Cleanup Order
Fixtures are cleaned up in reverse order (last created, first cleaned):
const ctx = buildHarness()
.use(() => ({
a: { create: () => "a", cleanup: () => console.log("cleanup a") },
}))
.use(() => ({
b: { create: () => "b", cleanup: () => console.log("cleanup b") },
}))
.use(() => ({
c: { create: () => "c", cleanup: () => console.log("cleanup c") },
}))
.setup()
// After tests:
// cleanup c
// cleanup b
// cleanup aType Inference
The context is fully typed:
const ctx = buildHarness()
.use(() => ({
pg: postgresContainer(),
}))
.use(({ pg }) => ({
prisma: bootstrapPrisma({ /* ... */ }),
}))
.setup()
// ctx is typed as:
// {
// pg: PostgresContainer
// prisma: PrismaClient
// }
ctx.pg.connectionString // ✅ string
ctx.prisma.task.findMany() // ✅ typed
ctx.unknown // ❌ Property 'unknown' does not existCommon Patterns
Reset Between Tests
const ctx = buildHarness()
.use(() => ({ pg: postgresContainer() }))
.use(({ pg }) => ({
prisma: bootstrapPrisma({ /* ... */ }),
}))
.setup()
describe("tests", () => {
beforeEach(async () => {
// Clean tables between tests
await ctx.prisma.task.deleteMany()
await ctx.prisma.user.deleteMany()
})
it("test 1", async () => { /* ... */ })
it("test 2", async () => { /* ... */ })
})Shared Test Data
const ctx = buildHarness()
.use(() => ({ pg: postgresContainer() }))
.use(({ pg }) => ({
prisma: bootstrapPrisma({ /* ... */ }),
}))
.use(({ prisma }) => ({
testUser: {
create: async () => {
return prisma.user.create({
data: { email: "test@example.com" },
})
},
cleanup: async (user) => {
await prisma.user.delete({ where: { id: user.id } })
},
},
}))
.setup()
it("uses test user", async () => {
const tasks = await ctx.prisma.task.findMany({
where: { userId: ctx.testUser.id },
})
})Multiple Databases
const ctx = buildHarness()
.use(() => ({
mainDb: postgresContainer({ database: "main" }),
analyticsDb: postgresContainer({ database: "analytics" }),
}))
.use(({ mainDb, analyticsDb }) => ({
mainPrisma: bootstrapPrisma({
client: MainPrismaClient,
schemaPath: "./prisma/main.prisma",
connectionString: mainDb.connectionString,
}),
analyticsPrisma: bootstrapPrisma({
client: AnalyticsPrismaClient,
schemaPath: "./prisma/analytics.prisma",
connectionString: analyticsDb.connectionString,
}),
}))
.setup()