@geekmidas/testkit
v1.0.1
Published
> Type-safe testing utilities and database factories for modern TypeScript applications
Downloads
800
Readme
@geekmidas/testkit
Type-safe testing utilities and database factories for modern TypeScript applications
Overview
@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.
Key Features
- 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
Installation
npm install --save-dev @geekmidas/testkit
# or
pnpm add -D @geekmidas/testkit
# or
yarn add -D @geekmidas/testkitSubpath Exports
// 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';Quick Start
Database Factories with Kysely
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<Database, 'users'>(
'users',
({ attrs, faker }) => ({
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email(),
createdAt: new Date(),
...attrs,
})
),
post: KyselyFactory.createBuilder<Database, 'posts'>(
'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: '[email protected]',
});
const posts = await factory.insertMany(3, 'post', {
userId: user.id,
});
expect(posts).toHaveLength(3);
expect(posts[0].userId).toBe(user.id);
});
});With Objection.js
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' });Enhanced Faker
The testkit provides an enhanced faker instance with additional utilities for common test data patterns.
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 5kmTimer Utilities
Simple async wait utility for tests.
import { waitFor } from '@geekmidas/testkit/timer';
it('should process after delay', async () => {
startBackgroundProcess();
await waitFor(100); // Wait 100ms
expect(processComplete).toBe(true);
});OS Utilities
Vitest fixture for temporary directory creation with automatic cleanup.
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
});AWS Testing Utilities
Mock AWS Lambda contexts and API Gateway events for testing Lambda handlers.
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);
});
});Logger Testing Utilities
Create mock loggers for testing code that uses @geekmidas/logger.
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'
);
});
});Better Auth Testing
In-memory adapter for testing Better Auth without a real database.
import { memoryAdapter } from '@geekmidas/testkit/better-auth';
import { betterAuth } from 'better-auth';
describe('Authentication', () => {
const adapter = memoryAdapter({
debugLogs: false,
initialData: {
user: [{ id: '1', email: '[email protected]', 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: '[email protected]',
password: 'password123',
});
const data = adapter.getAllData();
expect(data.user).toHaveLength(2);
});
});Database Migration
TestKit includes utilities for managing test database migrations.
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?.();
});Vitest Transaction Isolation
TestKit provides Vitest-specific helpers for automatic transaction isolation. Each test runs in a transaction that is automatically rolled back after the test completes.
Basic Usage
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<Database>(
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
});Extending with Fixtures
Use extendWithFixtures to add factory and other fixtures to your tests:
import { test } from 'vitest';
import {
wrapVitestKyselyTransaction,
extendWithFixtures,
KyselyFactory,
} from '@geekmidas/testkit/kysely';
// Define builders
const builders = {
user: KyselyFactory.createBuilder<Database, 'users'>('users', ({ faker }) => ({
name: faker.person.fullName(),
email: faker.internet.email(),
})),
post: KyselyFactory.createBuilder<Database, 'posts'>('posts', ({ faker }) => ({
title: faker.lorem.sentence(),
content: faker.lorem.paragraphs(),
})),
};
// Create base test with transaction
const baseTest = wrapVitestKyselyTransaction<Database>(test, () => db);
// Extend with factory fixture
const it = extendWithFixtures<
Database,
{ factory: KyselyFactory<Database, typeof builders, {}> }
>(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);
});Multiple Fixtures
You can add multiple fixtures that all receive the transaction:
const it = extendWithFixtures<
Database,
{
factory: KyselyFactory<Database, typeof builders, {}>;
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);
});With Objection.js
import { wrapVitestObjectionTransaction, extendWithFixtures } from '@geekmidas/testkit/objection';
const baseTest = wrapVitestObjectionTransaction(test, () => knex);
const it = extendWithFixtures<{ factory: ObjectionFactory<typeof builders, {}> }>(
baseTest,
{
factory: (trx) => new ObjectionFactory(builders, {}, trx),
}
);Manual Transaction Isolation
For more control, you can manage transactions manually:
describe('User Service', () => {
let trx: Transaction<Database>;
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
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:
Defining Seeds with Kysely
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<Database, typeof builders, {}>;
db: Kysely<Database>
}) => {
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
});Defining Seeds with Objection.js
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<typeof builders, {}>;
db: Knex
}) => {
const user = await factory.insert('user', {
email: attrs.email || '[email protected]',
});
await factory.insert('profile', { userId: user.id });
return user;
}
),
};
const factory = new ObjectionFactory(builders, seeds, knex);
const user = await factory.seed('userWithProfile', { email: '[email protected]' });Seed Context Object
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:
// 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');
});API Reference
KyselyFactory
class KyselyFactory<DB, Builders, Seeds> {
constructor(
builders: Builders,
seeds: Seeds,
db: Kysely<DB> | ControlledTransaction<DB>
);
// Create a type-safe builder for a specific table
static createBuilder<DB, TableName extends keyof DB & string>(
table: TableName,
defaults?: (context: {
attrs: Partial<Insertable<DB[TableName]>>;
factory: KyselyFactory;
db: Kysely<DB>;
faker: FakerFactory;
}) => Partial<Insertable<DB[TableName]>> | Promise<...>,
autoInsert?: boolean
): BuilderFunction;
// Create a type-safe seed function
static createSeed<Seed extends FactorySeed>(
seedFn: (context: { attrs: Attrs; factory: Factory; db: DB }) => Promise<Result>
): Seed;
// Insert a single record
insert<K extends keyof Builders>(
builderName: K,
attrs?: Partial<BuilderAttrs>
): Promise<BuilderResult>;
// Insert multiple records
insertMany<K extends keyof Builders>(
count: number,
builderName: K,
attrs?: Partial<BuilderAttrs> | ((idx: number, faker: FakerFactory) => Partial<BuilderAttrs>)
): Promise<BuilderResult[]>;
// Execute a seed function
seed<K extends keyof Seeds>(
seedName: K,
attrs?: ExtractSeedAttrs<Seeds[K]> // Attrs type is extracted from seed function
): Promise<SeedResult>;
}Enhanced Faker
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;
};
}AWS Mocks
function createMockContext(): Context;
function createMockV1Event(overrides?: Partial<APIGatewayProxyEvent>): APIGatewayProxyEvent;
function createMockV2Event(overrides?: Partial<APIGatewayProxyEventV2>): APIGatewayProxyEventV2;Memory Adapter (Better Auth)
function memoryAdapter(config?: {
debugLogs?: boolean;
usePlural?: boolean;
initialData?: Record<string, any[]>;
}): DatabaseAdapter & {
clear(): void;
getAllData(): Record<string, any[]>;
getStore(): Map<string, any>;
};Testing Best Practices
- Use Transactions: Always wrap tests in transactions for isolation
- Create Minimal Data: Only create the data necessary for each test
- Use Seeds for Complex Scenarios: Encapsulate complex setups in seeds
- Leverage Type Safety: Let TypeScript catch schema mismatches
- Clean Up Resources: Always clean up database connections and transactions
- Reset Sequences: Call
faker.resetAllSequences()inbeforeEachfor predictable IDs
Contributing
We welcome contributions! Please see our Contributing Guide for details.
License
This project is licensed under the MIT License - see the LICENSE file for details.
