Lawhive Framework
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/test

Options

OptionTypeDefaultDescription
imagestring"postgres:16-alpine"Docker image
databasestring"test"Database name
usernamestring"test"Username
passwordstring"test"Password

Return Value

type PostgresContainer = {
  connectionString: string
}

Container Lifecycle

The container:

  1. Starts in beforeAll with wait strategy for "ready to accept connections"
  2. Provides a unique port mapped to 5432
  3. 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

OptionTypeRequiredDescription
clientPrismaClientClassYesThe PrismaClient class (not instance)
schemaPathstringYesPath to schema.prisma file
connectionStringstringYesDatabase connection URL
prismaBinstringNoPath to Prisma CLI binary

How It Works

  1. Runs prisma db push --schema=<path> with the connection string
  2. Creates a new PrismaClient instance with datasourceUrl
  3. Disconnects in cleanup

Prisma Binary Resolution

If prismaBin is not provided, it searches:

  1. Walk up from schemaPath looking for node_modules/.bin/prisma
  2. 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 container

This 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()