@kysera/testing
Testing utilities for Kysera - transaction isolation, factories, seeding, and test helpers.
Installation
Install as a development dependency:
npm install --save-dev @kysera/testing
Overview
Version: 0.7.0 Bundle Size: ~6 KB (minified) Dependencies: None (peer: kysely >=0.28.8)
This is a utility package for testing. It's not part of the Repository/DAL pattern - it provides testing helpers that work with Kysely instances directly.
Key Features
- Transaction Rollback Testing - Automatic rollback for isolated, fast tests
- Database Cleanup Strategies - Multiple strategies for cleaning test databases
- Test Data Factories - Generate test data with sensible defaults
- Database Seeding - Composable seeders for consistent test data
- Test Helpers - Utilities for assertions, waiting, and snapshots
Quick Start
import { testInTransaction, createFactory } from '@kysera/testing';
const createUser = createFactory({
email: () => `user-${Date.now()}@example.com`,
name: 'Test User',
role: 'user',
});
it('creates user', async () => {
await testInTransaction(db, async (trx) => {
const userData = createUser({ name: 'Alice' });
const user = await trx.insertInto('users').values(userData).returningAll().executeTakeFirst();
expect(user?.name).toBe('Alice');
});
// Database automatically rolled back - no cleanup needed!
});
Transaction Testing
testInTransaction()
Test in a transaction that automatically rolls back. Fastest testing approach.
import { testInTransaction } from '@kysera/testing';
it('creates and queries user', async () => {
await testInTransaction(db, async (trx) => {
await trx.insertInto('users').values({ email: 'test@example.com', name: 'Test' }).execute();
const user = await trx.selectFrom('users').where('email', '=', 'test@example.com').selectAll().executeTakeFirst();
expect(user?.name).toBe('Test');
});
// Automatically rolled back
});
testWithSavepoints()
Test with savepoints for nested transaction testing.
import { testWithSavepoints } from '@kysera/testing';
it('handles nested operations', async () => {
await testWithSavepoints(db, async (trx) => {
await createUserWithProfile(trx, userData);
// Verify results...
});
});
testWithIsolation()
Test with specific transaction isolation level.
import { testWithIsolation } from '@kysera/testing';
it('handles serializable isolation', async () => {
await testWithIsolation(db, 'serializable', async (trx) => {
// Test behavior under serializable isolation
});
});
Isolation Levels: 'read uncommitted', 'read committed', 'repeatable read', 'serializable'
Database Cleanup
cleanDatabase()
Clean database using specified strategy.
import { cleanDatabase } from '@kysera/testing';
// Truncate - fast bulk cleanup
afterEach(async () => {
await cleanDatabase(db, 'truncate', ['users', 'orders', 'order_items']);
});
// Delete - requires FK-safe order (children first)
afterEach(async () => {
await cleanDatabase(db, 'delete', ['order_items', 'orders', 'users']);
});
Strategies:
'transaction'- No cleanup (use withtestInTransaction)'delete'- DELETE FROM each table (medium speed, FK-safe order required)'truncate'- TRUNCATE TABLE (fastest bulk clean, handles FKs automatically)
Test Data Factories
createFactory()
Create a generic test data factory.
import { createFactory } from '@kysera/testing';
const createUser = createFactory({
email: () => `user-${Date.now()}@example.com`,
name: 'Test User',
role: 'user',
});
const user1 = createUser(); // Use defaults
const admin = createUser({ role: 'admin' }); // Override
createMany()
Create multiple instances.
import { createMany } from '@kysera/testing';
const users = createMany(createUser, 5);
const admins = createMany(createUser, 3, (i) => ({
name: `Admin ${i + 1}`,
role: 'admin',
}));
createSequenceFactory()
Factory with built-in sequence counter.
import { createSequenceFactory } from '@kysera/testing';
const createUser = createSequenceFactory((seq) => ({
id: seq,
email: `user-${seq}@example.com`,
name: `User ${seq}`,
}));
const user1 = createUser(); // { id: 1, email: 'user-1@...' }
const user2 = createUser(); // { id: 2, email: 'user-2@...' }
Database Seeding
seedDatabase()
Seed database with test data.
import { seedDatabase } from '@kysera/testing';
beforeAll(async () => {
await seedDatabase(db, async (trx) => {
await trx.insertInto('users').values([
{ email: 'alice@example.com', name: 'Alice' },
{ email: 'bob@example.com', name: 'Bob' },
]).execute();
});
});
composeSeeders()
Combine multiple seed functions.
import { composeSeeders, seedDatabase, type SeedFunction } from '@kysera/testing';
const seedUsers: SeedFunction<DB> = async (trx) => {
await trx.insertInto('users').values([...]).execute();
};
const seedPosts: SeedFunction<DB> = async (trx) => {
await trx.insertInto('posts').values([...]).execute();
};
const seedAll = composeSeeders([seedUsers, seedPosts]);
beforeAll(async () => {
await seedDatabase(db, seedAll);
});
Test Helpers
waitFor()
Wait for a condition to be true.
import { waitFor } from '@kysera/testing';
await waitFor(async () => {
const user = await db.selectFrom('users').where('email', '=', 'test@example.com').executeTakeFirst();
return user !== undefined;
});
// With options
await waitFor(
async () => (await getProcessedCount()) >= 10,
{ timeout: 10000, interval: 200, timeoutMessage: 'Jobs did not complete' }
);
snapshotTable()
Snapshot table state for comparison.
import { snapshotTable } from '@kysera/testing';
const before = await snapshotTable(db, 'users');
await createUser(db, userData);
const after = await snapshotTable(db, 'users');
expect(after.length).toBe(before.length + 1);
countRows()
Count rows in a table.
import { countRows } from '@kysera/testing';
const count = await countRows(db, 'users');
expect(count).toBe(5);
assertRowExists()
Assert that a row exists.
import { assertRowExists } from '@kysera/testing';
const user = await assertRowExists(db, 'users', { email: 'test@example.com' });
expect(user.name).toBe('Test User');
assertRowNotExists()
Assert that no row exists.
import { assertRowNotExists } from '@kysera/testing';
await deleteUser(db, userId);
await assertRowNotExists(db, 'users', { id: userId });
TypeScript Types
type IsolationLevel = 'read uncommitted' | 'read committed' | 'repeatable read' | 'serializable';
type CleanupStrategy = 'truncate' | 'transaction' | 'delete';
type FactoryFunction<T> = (overrides?: Partial<T>) => T;
type SeedFunction<DB> = (trx: Transaction<DB>) => Promise<void>;
interface WaitForOptions {
timeout?: number; // Default: 5000
interval?: number; // Default: 100
timeoutMessage?: string;
}
Best Practices
1. Use Transaction Rollback for Speed
// Fast - automatic rollback
await testInTransaction(db, async (trx) => { /* test */ });
// Slower - manual cleanup
await createUser(db, userData);
await cleanDatabase(db, 'truncate', ['users']);
2. Define Factories Once
// factories.ts
export const createUser = createFactory({
email: () => `user-${Date.now()}@example.com`,
name: 'Test User',
});
// test file
import { createUser } from './factories';
3. Compose Seeders
// seeders.ts
export const seedUsers: SeedFunction<DB> = async (trx) => { ... };
export const seedPosts: SeedFunction<DB> = async (trx) => { ... };
export const seedAll = composeSeeders([seedUsers, seedPosts]);
4. Choose Right Cleanup Strategy
- Transaction: Fastest (use with
testInTransaction) - Truncate: Fast bulk cleanup, handles FKs automatically
- Delete: Medium speed, requires FK-safe order