Testing

@nuxt/test-utils

Boot the built app against a test database in an end-to-end suite, with the variables handed to setup().

An end-to-end suite with @nuxt/test-utils builds the app and boots it in a process of its own, which reaches the test database only through a URL. Serve it over the socket, create it before setup(), and hand setup() its variables:

test/e2e/items.test.ts
import { afterAll, describe, expect, it } from 'vitest'
import { $fetch, setup } from '@nuxt/test-utils/e2e'
import { createTestDatabase } from 'nuxt-pglite/testing'

describe('items', async () => {
  // Before `setup()`, which reads `env` when it is called.
  const db = await createTestDatabase({
    config: 'server/pglite.config',
    socket: true,
    exportEnv: false,
  })
  // Registered before `setup()`'s own, so it runs after the app is stopped.
  afterAll(() => db.close())

  await setup({ env: db.env })

  it('reads the rows the test wrote', async () => {
    await db.pg.query("INSERT INTO items (name) VALUES ('written by the test')")
    expect(await $fetch('/api/items')).toEqual([{ name: 'written by the test' }])
  })
})

The test writes through db.pg, in its own process, and the app reads the same rows through the socket.

Which apps it fits

The app's database code has to read the URL: the PGlite in dev, Postgres in prod setup, with the server side disabled and the development socket on. A route like this one reaches the test database unchanged:

server/api/items.get.ts
import pg from 'pg'

export default defineEventHandler(async () => {
  const client = new pg.Client({ connectionString: process.env.DATABASE_URL })
  await client.connect()
  try {
    const { rows } = await client.query('SELECT name FROM items ORDER BY id')
    return rows
  } finally {
    await client.end()
  }
})

With the server side enabled, usePGlite() in the built app creates its own instance from the config, not the test one.

The order of things

  • Create before setup(): setup() builds and starts the app with the env it is given, so the URL has to exist by then.
  • exportEnv: false: the variables reach the app through env alone, and the test process's process.env stays as it was. db.env holds them either way.
  • afterAll before setup(): vitest runs afterAll hooks in the reverse order of their registration (its default sequence.hooks), so the database is closed after setup()'s own hook has stopped the app, and the app never loses its database while still running.

Between tests

The app is started once per suite, behind one URL, so forking per test does not reach it. Reset the tables the tests write to instead:

import { beforeEach } from 'vitest'

beforeEach(() => db.pg.exec('TRUNCATE items RESTART IDENTITY'))