> Type-safe testing utilities and database factories for modern TypeScript applications
npm install @geekmidas/testkit> Type-safe testing utilities and database factories for modern TypeScript applications



@geekmidas/testkit provides a comprehensive set of testing utilities designed to simplify database testing in TypeScript applications. It offers factory patterns for creating test data, supports multiple database libraries, and ensures type safety throughout your tests.
- Factory Pattern: Create test data with minimal boilerplate
- Type Safety: Full TypeScript support with automatic schema inference
- Multi-Database Support: Works with Kysely and Objection.js
- Transaction Isolation: Built-in support for test isolation
- Enhanced Faker: Extended faker with timestamps, sequences, and coordinates
- AWS Mocks: Mock Lambda contexts and API Gateway events
- Better Auth: In-memory adapter for authentication testing
``bash`
npm install --save-dev @geekmidas/testkitor
pnpm add -D @geekmidas/testkitor
yarn add -D @geekmidas/testkit
`typescript
// Kysely utilities
import {
KyselyFactory,
wrapVitestKyselyTransaction,
extendWithFixtures,
} from '@geekmidas/testkit/kysely';
// Objection.js utilities
import {
ObjectionFactory,
wrapVitestObjectionTransaction,
extendWithFixtures,
} from '@geekmidas/testkit/objection';
// Other utilities
import { faker } from '@geekmidas/testkit/faker';
import { waitFor } from '@geekmidas/testkit/timer';
import { itWithDir } from '@geekmidas/testkit/os';
import { createMockContext, createMockV1Event, createMockV2Event } from '@geekmidas/testkit/aws';
import { createMockLogger } from '@geekmidas/testkit/logger';
import { memoryAdapter } from '@geekmidas/testkit/better-auth';
`
`typescript
import { KyselyFactory } from '@geekmidas/testkit/kysely';
import { Kysely } from 'kysely';
// Define your database schema
interface Database {
users: {
id: string;
name: string;
email: string;
createdAt: Date;
};
posts: {
id: string;
title: string;
content: string;
userId: string;
};
}
// Create builders for your tables
const builders = {
user: KyselyFactory.createBuilder
'users',
({ attrs, faker }) => ({
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email(),
createdAt: new Date(),
...attrs,
})
),
post: KyselyFactory.createBuilder
'posts',
({ attrs, faker }) => ({
id: faker.string.uuid(),
title: 'Test Post',
content: faker.lorem.paragraph(),
...attrs,
})
),
};
// Initialize factory
const factory = new KyselyFactory(builders, {}, db);
// Use in tests
describe('User Service', () => {
it('should create a user with posts', async () => {
const user = await factory.insert('user', {
name: 'Jane Smith',
email: 'jane@example.com',
});
const posts = await factory.insertMany(3, 'post', {
userId: user.id,
});
expect(posts).toHaveLength(3);
expect(posts[0].userId).toBe(user.id);
});
});
`
`typescript
import { ObjectionFactory } from '@geekmidas/testkit/objection';
import { Model } from 'objection';
class User extends Model {
static tableName = 'users';
id!: string;
name!: string;
email!: string;
}
const builders = {
user: ObjectionFactory.createBuilder(
User,
({ attrs, faker }) => ({
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email(),
...attrs,
})
),
};
const factory = new ObjectionFactory(builders, {}, knex);
const user = await factory.insert('user', { name: 'Jane Doe' });
`
The testkit provides an enhanced faker instance with additional utilities for common test data patterns.
`typescript
import { faker } from '@geekmidas/testkit/faker';
// Standard faker methods
const name = faker.person.fullName();
const email = faker.internet.email();
// Generate timestamps for database records
const { createdAt, updatedAt } = faker.timestamps();
// createdAt: Date in the past
// updatedAt: Date between createdAt and now
// Sequential numbers (useful for unique IDs)
faker.sequence(); // 1
faker.sequence(); // 2
faker.sequence('user'); // 1 (separate sequence)
faker.sequence('user'); // 2
// Reset sequences between tests
faker.resetSequence('user');
faker.resetAllSequences();
// Generate prices as numbers
const price = faker.price(); // 29.99
// Generate reverse domain identifiers
faker.identifier(); // "com.example.widget1"
faker.identifier('user'); // "org.acme.user"
// Generate coordinates within/outside a radius
const center = { lat: 40.7128, lng: -74.0060 };
faker.coordinates.within(center, 1000); // Within 1km
faker.coordinates.outside(center, 1000, 5000); // Between 1km and 5km
`
Simple async wait utility for tests.
`typescript
import { waitFor } from '@geekmidas/testkit/timer';
it('should process after delay', async () => {
startBackgroundProcess();
await waitFor(100); // Wait 100ms
expect(processComplete).toBe(true);
});
`
Vitest fixture for temporary directory creation with automatic cleanup.
`typescript
import { itWithDir } from '@geekmidas/testkit/os';
// Creates a temp directory before test, removes it after
itWithDir('should write files to temp dir', async ({ dir }) => {
const filePath = path.join(dir, 'test.txt');
await fs.writeFile(filePath, 'hello');
const content = await fs.readFile(filePath, 'utf-8');
expect(content).toBe('hello');
// Directory is automatically cleaned up after test
});
`
Mock AWS Lambda contexts and API Gateway events for testing Lambda handlers.
`typescript
import {
createMockContext,
createMockV1Event,
createMockV2Event
} from '@geekmidas/testkit/aws';
describe('Lambda Handler', () => {
it('should handle API Gateway v1 event', async () => {
const event = createMockV1Event({
httpMethod: 'POST',
path: '/users',
body: JSON.stringify({ name: 'John' }),
});
const context = createMockContext();
const result = await handler(event, context);
expect(result.statusCode).toBe(201);
});
it('should handle API Gateway v2 event', async () => {
const event = createMockV2Event({
routeKey: 'POST /users',
rawPath: '/users',
body: JSON.stringify({ name: 'John' }),
});
const context = createMockContext();
const result = await handler(event, context);
expect(result.statusCode).toBe(201);
});
});
`
Create mock loggers for testing code that uses @geekmidas/logger.
`typescript
import { createMockLogger } from '@geekmidas/testkit/logger';
describe('Service', () => {
it('should log errors', async () => {
const logger = createMockLogger();
const service = new MyService(logger);
await service.doSomethingRisky();
expect(logger.error).toHaveBeenCalledWith(
expect.objectContaining({ error: expect.any(Error) }),
'Operation failed'
);
});
});
`
In-memory adapter for testing Better Auth without a real database.
`typescript
import { memoryAdapter } from '@geekmidas/testkit/better-auth';
import { betterAuth } from 'better-auth';
describe('Authentication', () => {
const adapter = memoryAdapter({
debugLogs: false,
initialData: {
user: [{ id: '1', email: 'test@example.com', name: 'Test User' }],
},
});
const auth = betterAuth({
database: adapter,
// ... other config
});
afterEach(() => {
adapter.clear(); // Reset data between tests
});
it('should create user', async () => {
await auth.api.signUp({
email: 'new@example.com',
password: 'password123',
});
const data = adapter.getAllData();
expect(data.user).toHaveLength(2);
});
});
`
TestKit includes utilities for managing test database migrations.
`typescript
import { PostgresKyselyMigrator } from '@geekmidas/testkit/kysely';
const migrator = new PostgresKyselyMigrator({
database: 'test_db',
connection: {
host: 'localhost',
port: 5432,
user: 'postgres',
password: 'password',
},
migrationFolder: './migrations',
});
// In test setup
beforeAll(async () => {
const cleanup = await migrator.start();
globalThis.cleanupDb = cleanup;
});
afterAll(async () => {
await globalThis.cleanupDb?.();
});
`
TestKit provides Vitest-specific helpers for automatic transaction isolation. Each test runs in a transaction that is automatically rolled back after the test completes.
`typescript
import { test } from 'vitest';
import { wrapVitestKyselyTransaction } from '@geekmidas/testkit/kysely';
import { db } from './database';
// Wrap Vitest's test function with transaction support
const it = wrapVitestKyselyTransaction
test,
() => db,
async (trx) => {
// Optional: Set up test tables or seed data
await trx.schema.createTable('users').execute();
}
);
// Each test gets its own transaction
it('should create user', async ({ trx }) => {
const user = await trx
.insertInto('users')
.values({ name: 'John' })
.returningAll()
.executeTakeFirst();
expect(user.name).toBe('John');
// Transaction is automatically rolled back after test
});
`
Use extendWithFixtures to add factory and other fixtures to your tests:
`typescript
import { test } from 'vitest';
import {
wrapVitestKyselyTransaction,
extendWithFixtures,
KyselyFactory,
} from '@geekmidas/testkit/kysely';
// Define builders
const builders = {
user: KyselyFactory.createBuilder
name: faker.person.fullName(),
email: faker.internet.email(),
})),
post: KyselyFactory.createBuilder
title: faker.lorem.sentence(),
content: faker.lorem.paragraphs(),
})),
};
// Create base test with transaction
const baseTest = wrapVitestKyselyTransaction
// Extend with factory fixture
const it = extendWithFixtures<
Database,
{ factory: KyselyFactory
>(baseTest, {
factory: (trx) => new KyselyFactory(builders, {}, trx),
});
// Both trx and factory are available in tests
it('should create user with factory', async ({ trx, factory }) => {
const user = await factory.insert('user', { name: 'Jane' });
expect(user.id).toBeDefined();
expect(user.name).toBe('Jane');
// Verify in database
const found = await trx
.selectFrom('users')
.where('id', '=', user.id)
.selectAll()
.executeTakeFirst();
expect(found?.name).toBe('Jane');
});
it('should create related records', async ({ factory }) => {
const user = await factory.insert('user');
const posts = await factory.insertMany(3, 'post', { userId: user.id });
expect(posts).toHaveLength(3);
expect(posts[0].userId).toBe(user.id);
});
`
You can add multiple fixtures that all receive the transaction:
`typescript
const it = extendWithFixtures<
Database,
{
factory: KyselyFactory
userRepo: UserRepository;
config: { maxUsers: number };
}
>(baseTest, {
factory: (trx) => new KyselyFactory(builders, {}, trx),
userRepo: (trx) => new UserRepository(trx),
config: () => ({ maxUsers: 100 }), // Fixtures can ignore trx if not needed
});
it('should use multiple fixtures', async ({ factory, userRepo, config }) => {
const user = await factory.insert('user');
const found = await userRepo.findById(user.id);
expect(found).toBeDefined();
expect(config.maxUsers).toBe(100);
});
`
`typescript
import { wrapVitestObjectionTransaction, extendWithFixtures } from '@geekmidas/testkit/objection';
const baseTest = wrapVitestObjectionTransaction(test, () => knex);
const it = extendWithFixtures<{ factory: ObjectionFactory
baseTest,
{
factory: (trx) => new ObjectionFactory(builders, {}, trx),
}
);
`
For more control, you can manage transactions manually:
`typescript
describe('User Service', () => {
let trx: Transaction
let factory: KyselyFactory;
beforeEach(async () => {
trx = await db.transaction();
factory = new KyselyFactory(builders, seeds, trx);
});
afterEach(async () => {
await trx.rollback();
});
it('should perform operations in isolation', async () => {
const user = await factory.insert('user');
// All changes will be rolled back after the test
});
});
`
Seeds are functions that create complex test scenarios. Use createSeed to define type-safe seed functions that receive { attrs, factory, db } as a single context object:
`typescript
import { KyselyFactory } from '@geekmidas/testkit/kysely';
// Define seeds using createSeed for type safety
const seeds = {
// Simple seed with typed attrs
adminUser: KyselyFactory.createSeed(
async ({ attrs, factory }: {
attrs: { name?: string };
factory: KyselyFactory
db: Kysely
}) => {
return factory.insert('user', {
name: attrs.name || 'Admin User',
role: 'admin',
});
}
),
// Complex seed creating related records
blogWithPosts: KyselyFactory.createSeed(
async ({ attrs, factory }) => {
const author = await factory.insert('user', {
name: attrs.authorName || 'Blog Author',
role: 'author',
});
const categories = await factory.insertMany(3, 'category');
const posts = await factory.insertMany(attrs.postCount || 5, 'post', (index) => ({
title: Post ${index + 1},
authorId: author.id,
categoryId: categories[index % categories.length].id,
}));
return { author, categories, posts };
}
),
};
// Create factory with seeds
const factory = new KyselyFactory(builders, seeds, db);
// Use in tests - attrs are type-safe
const admin = await factory.seed('adminUser', { name: 'Super Admin' });
const blog = await factory.seed('blogWithPosts', {
authorName: 'Jane Doe',
postCount: 10
});
`
`typescript
import { ObjectionFactory } from '@geekmidas/testkit/objection';
import type { Knex } from 'knex';
const seeds = {
userWithProfile: ObjectionFactory.createSeed(
async ({ attrs, factory, db }: {
attrs: { email?: string };
factory: ObjectionFactory
db: Knex
}) => {
const user = await factory.insert('user', {
email: attrs.email || 'user@example.com',
});
await factory.insert('profile', { userId: user.id });
return user;
}
),
};
const factory = new ObjectionFactory(builders, seeds, knex);
const user = await factory.seed('userWithProfile', { email: 'custom@example.com' });
`
All seed functions receive a single context object with three properties:
| Property | Description |
|----------|-------------|
| attrs | Configuration attributes passed to factory.seed() |factory
| | The factory instance for creating records |db
| | The database connection (Kysely or Knex transaction) |
This pattern allows you to destructure only what you need:
`typescript
// Use all three
KyselyFactory.createSeed(async ({ attrs, factory, db }) => {
// Direct db access for complex queries
const existing = await db.selectFrom('users').where('role', '=', 'admin').execute();
// ...
});
// Or just what you need
KyselyFactory.createSeed(async ({ factory }) => {
return factory.insert('user');
});
`
`typescript
class KyselyFactory
constructor(
builders: Builders,
seeds: Seeds,
db: Kysely
);
// Create a type-safe builder for a specific table
static createBuilder
table: TableName,
defaults?: (context: {
attrs: Partial
factory: KyselyFactory;
db: Kysely
faker: FakerFactory;
}) => Partial
autoInsert?: boolean
): BuilderFunction;
// Create a type-safe seed function
static createSeed
seedFn: (context: { attrs: Attrs; factory: Factory; db: DB }) => Promise
): Seed;
// Insert a single record
insert
builderName: K,
attrs?: Partial
): Promise
// Insert multiple records
insertMany
count: number,
builderName: K,
attrs?: Partial
): Promise
// Execute a seed function
seed
seedName: K,
attrs?: ExtractSeedAttrs
): Promise
}
`
`typescript`
interface EnhancedFaker extends Faker {
timestamps(): { createdAt: Date; updatedAt: Date };
sequence(name?: string): number;
resetSequence(name?: string, value?: number): void;
resetAllSequences(): void;
identifier(suffix?: string): string;
price(): number;
coordinates: {
within(center: Coordinate, radiusMeters: number): Coordinate;
outside(center: Coordinate, minRadius: number, maxRadius: number): Coordinate;
};
}
`typescript`
function createMockContext(): Context;
function createMockV1Event(overrides?: Partial
function createMockV2Event(overrides?: Partial
`typescript`
function memoryAdapter(config?: {
debugLogs?: boolean;
usePlural?: boolean;
initialData?: Record
}): DatabaseAdapter & {
clear(): void;
getAllData(): Record
getStore(): Map
};
1. Use Transactions: Always wrap tests in transactions for isolation
2. Create Minimal Data: Only create the data necessary for each test
3. Use Seeds for Complex Scenarios: Encapsulate complex setups in seeds
4. Leverage Type Safety: Let TypeScript catch schema mismatches
5. Clean Up Resources: Always clean up database connections and transactions
6. Reset Sequences: Call faker.resetAllSequences() in beforeEach` for predictable IDs
We welcome contributions! Please see our Contributing Guide for details.
This project is licensed under the MIT License - see the LICENSE file for details.