Testing

Isolation

Seed once, then fork the database per test, so each starts from the same data and none sees another's writes.

Tests that write to a shared database see each other's rows, and their outcome depends on the order they run in. db.fork() gives each test its own copy instead: a fresh in-memory database holding the current state of its parent, with the same extensions.

test/todos.test.ts
import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from 'vitest'
import { createTestDatabase } from 'nuxt-pglite/testing'
import type { TestDatabase } from 'nuxt-pglite/testing'

let db: TestDatabase
let test: TestDatabase

beforeAll(async () => {
  db = await createTestDatabase({ config: 'server/pglite.config' })
  await db.pg.exec("INSERT INTO todos (title) VALUES ('seeded')")
})
beforeEach(async () => {
  test = await db.fork()
})
afterEach(() => test.close())
afterAll(() => db.close())

it('adds a todo', async () => {
  await test.pg.query('INSERT INTO todos (title) VALUES ($1)', ['added'])
  const { rows } = await test.pg.query('SELECT count(*)::int AS total FROM todos')
  expect(rows).toEqual([{ total: 2 }])
})

it('starts from the seed again', async () => {
  const { rows } = await test.pg.query('SELECT title FROM todos')
  expect(rows).toEqual([{ title: 'seeded' }])
})

How a fork is made

The parent's data directory is dumped and loaded into a new PGlite instance, created with the config's options and extensions. init is not run again: the copy already holds what it created, together with whatever the parent wrote before forking. A fork is always in memory, whatever the parent's dataDir.

Each fork is a PGlite instance of its own. Tests that only read can share the parent rather than fork it.

Closing

A fork closes independently of its parent, and its close() does not run the config's dispose, which belongs to the instance init ran on. Closing the parent closes the forks still open, so afterAll(() => db.close()) is enough when a test fails before its own cleanup. A closed database cannot be forked any more: fork() throws.

Sockets

When the parent is served over the socket, each fork gets a socket of its own, with the same options on a free port, so that tests can run side by side. fork({ socket }) decides otherwise: false for none, true for a free port, or the socket options.

A fork never sets anything on process.env: its parent's variables are there already, and two forks would fight over them. Hand its url, or its env, to the code under test:

import pg from 'pg'

beforeEach(async () => {
  test = await db.fork()
})

it('connects to its own fork', async () => {
  const client = new pg.Client({ connectionString: test.url })
  await client.connect()
  // ...
  await client.end()
})

For code that reads DATABASE_URL itself, vitest's vi.stubEnv sets the fork's variables for the length of a test:

import { vi } from 'vitest'

beforeEach(async () => {
  test = await db.fork()
  for (const [name, value] of Object.entries(test.env)) {
    vi.stubEnv(name, value)
  }
})
afterEach(async () => {
  vi.unstubAllEnvs()
  await test.close()
})

This only reaches code that reads the variable when it connects; a pool created once at import time keeps the first URL it saw.