📚 5 min read
Test Utilities in TypeScript ​
This section provides a collection of type-safe test utilities and patterns for writing better tests in TypeScript.
Overview ​
Test utilities help you write more maintainable and type-safe tests while reducing boilerplate code.
Basic Test Utilities ​
Type-Safe Test Factory ​
typescript
type Factory<T> = {
build(overrides?: Partial<T>): T;
buildList(count: number, overrides?: Partial<T>): T[];
};
function createFactory<T>(defaults: T): Factory<T> {
return {
build(overrides = {}) {
return { ...defaults, ...overrides };
},
buildList(count, overrides = {}) {
return Array.from({ length: count }, () => this.build(overrides));
},
};
}
// Usage
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
const userFactory = createFactory<User>({
id: '1',
name: 'John Doe',
email: 'john@example.com',
role: 'user',
});
const user = userFactory.build({ role: 'admin' });
const users = userFactory.buildList(3, { role: 'user' });
Mock Data Generator ​
typescript
class MockDataGenerator {
private static counter = 0;
static uniqueId(): string {
return (++this.counter).toString();
}
static email(name?: string): string {
const base = name?.toLowerCase().replace(/\s+/g, '.') || 'user';
return `${base}.${this.uniqueId()}@example.com`;
}
static date(
options: {
min?: Date;
max?: Date;
} = {}
): Date {
const min = options.min?.getTime() ?? new Date(2000, 0).getTime();
const max = options.max?.getTime() ?? Date.now();
const timestamp = min + Math.random() * (max - min);
return new Date(timestamp);
}
static pick<T>(items: T[]): T {
return items[Math.floor(Math.random() * items.length)];
}
static shuffle<T>(items: T[]): T[] {
return [...items].sort(() => Math.random() - 0.5);
}
}
Advanced Utilities ​
Test Context Manager ​
typescript
interface TestContext<T> {
setup(): Promise<T>;
teardown(): Promise<void>;
}
class TestContextManager<T> {
private context: T | null = null;
constructor(private contextFactory: TestContext<T>) {}
async setup(): Promise<T> {
this.context = await this.contextFactory.setup();
return this.context;
}
async teardown(): Promise<void> {
if (this.context) {
await this.contextFactory.teardown();
this.context = null;
}
}
getContext(): T {
if (!this.context) {
throw new Error('Context not initialized');
}
return this.context;
}
}
// Usage with Jest
describe('User Tests', () => {
const ctx = new TestContextManager({
async setup() {
const db = await createTestDatabase();
const user = await db.users.create({
name: 'Test User',
email: MockDataGenerator.email(),
});
return { db, user };
},
async teardown() {
await db.close();
},
});
beforeEach(() => ctx.setup());
afterEach(() => ctx.teardown());
test('user operations', () => {
const { db, user } = ctx.getContext();
// Test implementation
});
});
Mock Service Generator ​
typescript
type MockMethod<T> = T extends (...args: any[]) => any
? jest.Mock<ReturnType<T>, Parameters<T>>
: T;
type MockService<T> = {
[K in keyof T]: MockMethod<T[K]>;
};
function createMockService<T extends object>(
service: new (...args: any[]) => T
): MockService<T> {
const prototype = service.prototype;
const methods = Object.getOwnPropertyNames(prototype).filter(
(name) => name !== 'constructor'
);
return methods.reduce((mock, method) => {
mock[method] = jest.fn();
return mock;
}, {} as MockService<T>);
}
// Usage
class UserService {
async getUser(id: string): Promise<User> {
// Implementation
}
async createUser(data: Omit<User, 'id'>): Promise<User> {
// Implementation
}
}
const mockUserService = createMockService(UserService);
mockUserService.getUser.mockResolvedValue({
id: '1',
name: 'Test User',
email: 'test@example.com',
});
Real-World Example ​
typescript
// Test utilities for a full application
class TestUtils {
// Database utilities
static async createTestDatabase() {
const db = await Database.connect({
url: process.env.TEST_DATABASE_URL,
logging: false,
});
await db.migrate();
return db;
}
static async clearDatabase(db: Database) {
const tables = await db.getTables();
for (const table of tables) {
await db.truncate(table);
}
}
// HTTP utilities
static createTestServer(
options: {
auth?: boolean;
middlewares?: Middleware[];
} = {}
) {
const app = express();
if (options.auth) {
app.use(authMiddleware);
}
if (options.middlewares) {
app.use(options.middlewares);
}
return app;
}
static async request(
app: Express,
options: {
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
url: string;
body?: unknown;
token?: string;
}
) {
const req = supertest(app)[options.method.toLowerCase()](options.url);
if (options.token) {
req.set('Authorization', `Bearer ${options.token}`);
}
if (options.body) {
req.send(options.body);
}
return req;
}
// Mock factories
static createUserFactory() {
return createFactory<User>({
id: MockDataGenerator.uniqueId(),
name: 'Test User',
email: MockDataGenerator.email(),
role: 'user',
});
}
static createPostFactory(authorId: string) {
return createFactory<Post>({
id: MockDataGenerator.uniqueId(),
title: 'Test Post',
content: 'Test Content',
authorId,
});
}
// Test context
static createTestContext() {
return new TestContextManager({
async setup() {
const db = await TestUtils.createTestDatabase();
const app = TestUtils.createTestServer({ auth: true });
const userFactory = TestUtils.createUserFactory();
const user = await db.users.create(userFactory.build());
const token = generateToken(user);
return {
db,
app,
user,
token,
factories: {
user: userFactory,
post: TestUtils.createPostFactory(user.id),
},
};
},
async teardown() {
await TestUtils.clearDatabase(this.db);
await this.db.close();
},
});
}
}
// Usage in tests
describe('API Tests', () => {
const ctx = TestUtils.createTestContext();
beforeEach(() => ctx.setup());
afterEach(() => ctx.teardown());
describe('POST /api/posts', () => {
test('creates post successfully', async () => {
const { app, token, factories } = ctx.getContext();
const postData = factories.post.build();
const response = await TestUtils.request(app, {
method: 'POST',
url: '/api/posts',
body: postData,
token,
});
expect(response.status).toBe(201);
expect(response.body).toMatchObject({
title: postData.title,
content: postData.content,
});
});
test('handles validation errors', async () => {
const { app, token } = ctx.getContext();
const invalidData = { title: '' };
const response = await TestUtils.request(app, {
method: 'POST',
url: '/api/posts',
body: invalidData,
token,
});
expect(response.status).toBe(400);
expect(response.body).toHaveProperty('errors');
});
});
describe('GET /api/posts', () => {
test('lists user posts', async () => {
const { app, token, db, factories } = ctx.getContext();
const posts = await Promise.all(
factories.post.buildList(3).map((post) => db.posts.create(post))
);
const response = await TestUtils.request(app, {
method: 'GET',
url: '/api/posts',
token,
});
expect(response.status).toBe(200);
expect(response.body).toHaveLength(posts.length);
});
});
});
Best Practices ​
Test Utilities:
- Keep utilities type-safe
- Make utilities reusable
- Document utility behavior
Test Data:
- Use factories for test data
- Generate unique values
- Clean up test data
Test Context:
- Manage test lifecycle
- Share common setup
- Handle cleanup properly