Testing

Run the application around each test, on a new database, as the caller of your choice.

testApplication(app) starts the application before each test of the file and stops it after: on a new in-memory database, and an HTTP server on a free port. Tests run with Vitest; install @ultimabase/testing as a dev dependency:

npm install -D @ultimabase/testing vitest
src/app.test.ts
// Starts the application before each test, on a new in-memory database and
// a free port, and stops it after.
import { asUser, testApplication } from '@ultimabase/testing'
import { master } from 'ultimabase'
import { expect, it } from 'vitest'
import { app } from './app.ts'
import { Note } from './models.ts'

const running = testApplication(app)

The full example, with its tests: examples/testing.

The application starts as it is declared, its plugins and functions included, but without its info lines: warnings and errors still print.

A new database each test

Each test starts on an empty database: a row one test writes is gone in the next, so tests run in any order.

src/app.test.ts
it('writes a note', async () => {
	await Note.create({ data: { text: 'Milk', ownerId: 'ada' } }, { as: master })
	expect(await Note.count({}, { as: master })).toBe(1)
})

it('starts on an empty database', async () => {
	expect(await Note.count({}, { as: master })).toBe(0)
})

As a signed-in user

asUser({ id }) is the caller a session gives, without signing in: its access rules apply, as in the application.

src/app.test.ts
// A caller as a session gives it, without signing in.
const ada = { as: asUser({ id: 'ada' }) }
const note = await Note.create({ data: { text: 'Milk' } }, ada)
expect(note.ownerId).toBe('ada')

const grace = { as: asUser({ id: 'grace' }) }
expect(await Note.findMany({}, grace)).toEqual([])

{ as: master } skips the rules, to seed rows or read them all. Without as, the caller is anonymous.

Over HTTP

running.url is the server's address during a test, such as http://localhost:54321: a test calls the API as a client does.

src/app.test.ts
await Note.create({ data: { text: 'Milk', ownerId: 'ada' } }, { as: master })
// `running.url` is the server's address during the test.
const response = await fetch(`${running.url}/graphql`, {
	method: 'POST',
	headers: { 'content-type': 'application/json' },
	body: JSON.stringify({ query: '{ notes { edges { node { text } } } }' }),
})
// Without a session, the caller is anonymous: no note is its own.
expect(await response.json()).toEqual({ data: { notes: { edges: [] } } })

On PostgreSQL

{ database: 'declared' } runs the tests on the application's own database: on postgres(), in a schema of the test file's own, named ultimabase_test_<random>. The first test creates the tables in it, each later one starts with them emptied, and the schema is dropped after the file's last test. No other table is read or written.

src/tasks.postgres.test.ts
// On the database DATABASE_URL names: the file's own schema, its tables
// emptied before each test, dropped after the last.
testApplication(app, { database: 'declared' })

it('deletes the tasks with their project', async () => {
	const project = await Project.create(
		{ data: { name: 'Launch', tasks: { create: [{ title: 'Write' }] } } },
		{ as: master },
	)
	await Project.delete({ where: { id: project.id } }, { as: master })
	expect(await Task.count({}, { as: master })).toBe(0)
})

it('starts with empty tables', async () => {
	expect(await Project.count({}, { as: master })).toBe(0)
})

A test on PostgreSQL checks what memory() cannot: raw SQL, a constraint, the real column types. It is slower: keep most tests on memory(), and name these *.postgres.test.ts to run them apart.

Reference

testApplication(app, options?)

OptionTypeDefaultEffect
database'declared'memory()The application's own database, in the file's own schema.

It returns { url }: the HTTP server's address, read during a test.

asUser(user)

ArgumentTypeReturns
user{ id }, or the registered user rowa caller of kind 'user', for { as }

Errors

MessageWhen
testApplication takes the value defineApp returned.Another value was given.
url is the address of httpPlugin(), read during a test: add httpPlugin() to the application.url read outside a test, or without HTTP.
testApplication runs on the declared database when it gives a test file tables of its own, as postgres() does; this one does not: remove { database: 'declared' }.The database cannot give a schema per file.

Limits

  • testApplication adds Vitest's beforeEach and afterEach: call it at the top of a file or a describe, never inside a test.
  • A process killed during a test file leaves its schema behind.

On this page