Testing
Fast tests on in-memory SQLite with a fresh client per test.
Use this guide to set up tests for code that uses better-drizzle. There is no test mode and no mock client: you test it the way it runs, better(db) on top of a real Drizzle database. For most suites that database is SQLite in memory, which is fast enough to build from scratch for every test.
The examples use bun:test. Vitest has the same describe, test, expect, beforeEach, and describe.skipIf, so the code carries over as is. Under Vitest on Node, use better-sqlite3 instead of bun:sqlite.
In-memory SQLite
Put the database setup in one helper that every test file imports. It opens a new :memory: database, applies the schema, and returns a client:
import { Database } from 'bun:sqlite';
import { better } from 'better-drizzle';
import { drizzle } from 'drizzle-orm/bun-sqlite';
import { relations } from '../src/db/relations';
const ddl = `
CREATE TABLE users (
id INTEGER PRIMARY KEY NOT NULL,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
active INTEGER NOT NULL
);
`;
export const createTestDb = () => {
const sqlite = new Database(':memory:');
sqlite.exec('PRAGMA foreign_keys = ON;');
sqlite.exec(ddl);
const db = drizzle({ client: sqlite, relations });
const client = better(db);
return { client, db, close: () => sqlite.close() };
};import Database from 'better-sqlite3';
import { better } from 'better-drizzle';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import { relations } from '../src/db/relations';
const ddl = `
CREATE TABLE users (
id INTEGER PRIMARY KEY NOT NULL,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
active INTEGER NOT NULL
);
`;
export const createTestDb = () => {
const sqlite = new Database(':memory:');
sqlite.exec('PRAGMA foreign_keys = ON;');
sqlite.exec(ddl);
const db = drizzle({ client: sqlite, relations });
const client = better(db);
return { client, db, close: () => sqlite.close() };
};SQLite leaves foreign keys off by default. Turn them on so tests fail on the same constraint violations as production.
Applying the schema
Any way that produces the right tables works. Pick one and keep it in the helper:
- Raw DDL in the test helper. It is the fastest option, and you see exactly what the tests run against. The downside is that it can drift from your real migrations.
- Your drizzle-kit migrations. Run the generated SQL files with Drizzle's migrator, so tests use the same schema history as production (see below).
drizzle-kit pushagainst a database that lives longer than one test, such as a PostgreSQL test database. It cannot target a:memory:database, because that database only exists inside the test process.
import { migrate } from 'drizzle-orm/node-postgres/migrator';
await migrate(db, { migrationsFolder: 'drizzle' });import { drizzle } from 'drizzle-orm/mysql2';
import { migrate } from 'drizzle-orm/mysql2/migrator';
const db = drizzle(process.env.DATABASE_URL ?? '');
await migrate(db, { migrationsFolder: 'drizzle' });import { Database } from 'bun:sqlite';
import { drizzle } from 'drizzle-orm/bun-sqlite';
import { migrate } from 'drizzle-orm/bun-sqlite/migrator';
const db = drizzle({ client: new Database(':memory:') });
migrate(db, { migrationsFolder: 'drizzle' });On Node, import from drizzle-orm/better-sqlite3/migrator.
import { migrate } from 'drizzle-orm/node-postgres/migrator';
await migrate(db, { migrationsFolder: 'drizzle' });import { drizzle } from 'drizzle-orm/mysql2';
import { migrate } from 'drizzle-orm/mysql2/migrator';
const db = drizzle(process.env.DATABASE_URL ?? '');
await migrate(db, { migrationsFolder: 'drizzle' });import { Database } from 'bun:sqlite';
import { drizzle } from 'drizzle-orm/bun-sqlite';
import { migrate } from 'drizzle-orm/bun-sqlite/migrator';
const db = drizzle({ client: new Database(':memory:') });
migrate(db, { migrationsFolder: 'drizzle' });On Node, import from drizzle-orm/better-sqlite3/migrator.
A fresh client per test
Create the database in beforeEach and close it in afterEach. Tests then share nothing, which means no ordering bugs and no cleanup code. The code under test takes the client as a parameter:
import type { BetterDrizzleClient } from 'better-drizzle';
import type { relations } from './db/relations';
export const deactivateUser = (
client: BetterDrizzleClient<typeof relations>,
id: number,
) => client.users.update({ where: { id }, data: { active: false } });import { afterEach, beforeEach, expect, test } from 'bun:test';
import { deactivateUser } from '../src/users';
import { createTestDb } from './db';
let ctx: ReturnType<typeof createTestDb>;
beforeEach(async () => {
ctx = createTestDb();
await ctx.client.users.createMany({
data: [
{ id: 1, email: 'alice@example.com', name: 'Alice', active: true },
{ id: 2, email: 'bob@example.com', name: 'Bob', active: true },
],
});
});
afterEach(() => ctx.close());
test('deactivateUser flips the flag', async () => {
await deactivateUser(ctx.client, 1);
const user = await ctx.client.users.findUnique({ where: { id: 1 } });
expect(user?.active).toBe(false);
});To test code this way, it has to take the client as a parameter or from a factory. A module that imports a process-wide client directly cannot be pointed at a test database.
better(...) runs every plugin's setup() when it builds the client. With a fresh client per test, that happens once per test. On an in-memory database this costs very little.
Seeding
Seed with createMany. It goes through the same path as your application writes, including plugins such as timestamps:
const { count } = await ctx.client.users.createMany({
data: [
{ id: 1, email: 'alice@example.com', name: 'Alice', active: true },
{ id: 2, email: 'bob@example.com', name: 'Bob', active: true },
],
});
expect(count).toBe(2);If a plugin should not touch fixture rows, seed through ctx.client.users.$withoutPlugins().createMany(...), or insert with the raw Drizzle db.
To assert on hooks, plugins, and errors, see test assertions. For a shared PostgreSQL database, see testing on PostgreSQL.
Awaiting reads
Read methods (findMany, findFirst, findOne, findUnique, count, exists, paginate, cursor) return lazy thenables, like Drizzle's query builders: the query runs when you await it. Bun's expect(...).resolves / .rejects only accept native promises, so await the read first, or wrap it with Promise.resolve(...). Helpers that already return promises, such as .throw() and .explain(), work with them directly:
expect(await ctx.client.users.findMany({ take: 2 })).toHaveLength(2);
// row locks are rejected on SQLite
await expect(
Promise.resolve(ctx.client.users.findMany({ lock: 'update' })),
).rejects.toMatchObject({ code: BetterDrizzleErrorCode.LockNotSupported });