Fixtures
Chapter 44: Test Fixtures & Factories
Section titled “Chapter 44: Test Fixtures & Factories”Creating Reusable Test Data
Section titled “Creating Reusable Test Data”44.1 Fixtures Overview
Section titled “44.1 Fixtures Overview”Test fixtures provide consistent, reusable test data across your test suite.
Fixture Architecture ================================================================================
+------------------+ | Test File | +------------------+ | v +------------------+ | Fixture | <-- Reusable data definitions | Factory | +------------------+ | v +------------------+ | Entity | <-- TypeORM entities +------------------+
Benefits: - Consistent test data - Easy to maintain - Reduces test setup code - Supports relationships
================================================================================44.2 Basic Fixture Factory
Section titled “44.2 Basic Fixture Factory”Simple Factory
Section titled “Simple Factory”import { User } from '../../src/users/user.entity';
export class UserFixture { private static counter = 0;
static create(overrides: Partial<User> = {}): User { this.counter++; const user = new User(); user.name = overrides.name || `Test User ${this.counter}`; user.email = overrides.email || `test${this.counter}@example.com`; user.password = overrides.password || 'hashed_password'; user.isActive = overrides.isActive ?? true; user.role = overrides.role || 'user'; return user; }
static createMany(count: number, overrides: Partial<User> = {}): User[] { return Array.from({ length: count }, () => this.create(overrides)); }
static reset() { this.counter = 0; }}
// Usageconst user = UserFixture.create({ name: 'John Doe' });const users = UserFixture.createMany(5, { isActive: true });Builder Pattern Factory
Section titled “Builder Pattern Factory”import { User } from '../../src/users/user.entity';
export class UserBuilder { private user: Partial<User> = { name: 'Default User', email: 'default@example.com', password: 'hashed_password', isActive: true, role: 'user', };
withName(name: string): UserBuilder { this.user.name = name; return this; }
withEmail(email: string): UserBuilder { this.user.email = email; return this; }
withPassword(password: string): UserBuilder { this.user.password = password; return this; }
asAdmin(): UserBuilder { this.user.role = 'admin'; return this; }
asInactive(): UserBuilder { this.user.isActive = false; return this; }
build(): User { const user = new User(); Object.assign(user, this.user); return user; }}
// Usageconst admin = new UserBuilder() .withName('Admin User') .withEmail('admin@example.com') .asAdmin() .build();
const inactiveUser = new UserBuilder() .withName('Inactive User') .asInactive() .build();44.3 Entity Fixtures with Relationships
Section titled “44.3 Entity Fixtures with Relationships”Related Entity Fixtures
Section titled “Related Entity Fixtures”import { Post } from '../../src/posts/post.entity';import { UserFixture } from './user.fixture';
export class PostFixture { private static counter = 0;
static create(overrides: Partial<Post> = {}): Post { this.counter++; const post = new Post(); post.title = overrides.title || `Test Post ${this.counter}`; post.content = overrides.content || 'Test content'; post.isPublished = overrides.isPublished ?? true; post.viewCount = overrides.viewCount || 0; return post; }
static createForUser(userId: number, overrides: Partial<Post> = {}): Post { const post = this.create(overrides); post.authorId = userId; return post; }
static createWithAuthor( userOverrides: Partial<User> = {}, postOverrides: Partial<Post> = {}, ): { post: Post; author: User } { const author = UserFixture.create(userOverrides); const post = this.create(postOverrides); post.author = author; return { post, author }; }
static createManyForUser( userId: number, count: number, overrides: Partial<Post> = {}, ): Post[] { return Array.from({ length: count }, () => this.createForUser(userId, overrides), ); }}
// Usageconst post = PostFixture.createForUser(1, { title: 'My First Post' });const { post, author } = PostFixture.createWithAuthor({ name: 'John' });Complex Relationship Fixtures
Section titled “Complex Relationship Fixtures”import { Comment } from '../../src/comments/comment.entity';import { UserFixture } from './user.fixture';import { PostFixture } from './post.fixture';
export class CommentFixture { private static counter = 0;
static create(overrides: Partial<Comment> = {}): Comment { this.counter++; const comment = new Comment(); comment.content = overrides.content || `Test comment ${this.counter}`; comment.isApproved = overrides.isApproved ?? true; return comment; }
static createFullScenario(): { author: User; commenter: User; post: Post; comment: Comment; } { const author = UserFixture.create({ name: 'Post Author' }); const commenter = UserFixture.create({ name: 'Commenter' }); const post = PostFixture.create({ title: 'Test Post' }); post.author = author;
const comment = this.create(); comment.post = post; comment.author = commenter;
return { author, commenter, post, comment }; }}44.4 Database Fixture Loader
Section titled “44.4 Database Fixture Loader”Fixture Loader Service
Section titled “Fixture Loader Service”import { DataSource, Repository } from 'typeorm';
export class FixtureLoader { constructor(private dataSource: DataSource) {}
async load<T>(entity: new () => T, data: Partial<T>[]): Promise<T[]> { const repository = this.dataSource.getRepository(entity) as Repository<T>; const entities = repository.create(data); return repository.save(entities); }
async loadOne<T>(entity: new () => T, data: Partial<T>): Promise<T> { const [result] = await this.load(entity, [data]); return result; }
async loadUsers(count: number = 5): Promise<User[]> { const users = UserFixture.createMany(count); return this.load(User, users); }
async loadPostsWithAuthors(postCount: number = 5): Promise<{ users: User[]; posts: Post[] }> { const users = await this.loadUsers(2); const posts = PostFixture.createMany(postCount).map((post, i) => ({ ...post, authorId: users[i % users.length].id, }));
const savedPosts = await this.load(Post, posts);
return { users, posts: savedPosts }; }
async loadFullBlogScenario(): Promise<{ users: User[]; posts: Post[]; comments: Comment[]; }> { // Create users const users = await this.loadUsers(3);
// Create posts const posts = await this.load(Post, [ PostFixture.createForUser(users[0].id, { title: 'Post 1' }), PostFixture.createForUser(users[0].id, { title: 'Post 2' }), PostFixture.createForUser(users[1].id, { title: 'Post 3' }), ]);
// Create comments const comments = await this.load(Comment, [ { ...CommentFixture.create(), postId: posts[0].id, authorId: users[1].id }, { ...CommentFixture.create(), postId: posts[0].id, authorId: users[2].id }, { ...CommentFixture.create(), postId: posts[1].id, authorId: users[1].id }, ]);
return { users, posts, comments }; }}
// Usage in testsdescribe('With FixtureLoader', () => { let loader: FixtureLoader;
beforeEach(async () => { loader = new FixtureLoader(dataSource); });
it('should load test data', async () => { const users = await loader.loadUsers(10); expect(users).toHaveLength(10); });
it('should load related data', async () => { const { users, posts } = await loader.loadPostsWithAuthors(5); expect(users).toHaveLength(2); expect(posts).toHaveLength(5); });});44.5 JSON Fixtures
Section titled “44.5 JSON Fixtures”JSON Fixture Files
Section titled “JSON Fixture Files”[ { "id": 1, "name": "John Doe", "email": "john@example.com", "isActive": true, "role": "admin" }, { "id": 2, "name": "Jane Doe", "email": "jane@example.com", "isActive": true, "role": "user" }, { "id": 3, "name": "Bob Smith", "email": "bob@example.com", "isActive": false, "role": "user" }]
// test/fixtures/data/posts.json[ { "id": 1, "title": "First Post", "content": "Content of first post", "authorId": 1, "isPublished": true }, { "id": 2, "title": "Second Post", "content": "Content of second post", "authorId": 1, "isPublished": true }]JSON Fixture Loader
Section titled “JSON Fixture Loader”import * as fs from 'fs';import * as path from 'path';import { DataSource } from 'typeorm';
export class JsonFixtureLoader { private fixturesPath: string;
constructor( private dataSource: DataSource, fixturesPath: string = path.join(__dirname, 'data'), ) { this.fixturesPath = fixturesPath; }
async load(entityName: string): Promise<any[]> { const filePath = path.join(this.fixturesPath, `${entityName}.json`);
if (!fs.existsSync(filePath)) { throw new Error(`Fixture file not found: ${filePath}`); }
const data = JSON.parse(fs.readFileSync(filePath, 'utf8')); const entity = this.getEntityByName(entityName);
return this.dataSource.getRepository(entity).save(data); }
async loadAll(entityNames: string[]): Promise<Record<string, any[]>> { const results: Record<string, any[]> = {};
for (const name of entityNames) { results[name] = await this.load(name); }
return results; }
private getEntityByName(name: string): any { const entityMap: Record<string, any> = { users: User, posts: Post, comments: Comment, };
return entityMap[name]; }}
// Usagedescribe('With JSON Fixtures', () => { let loader: JsonFixtureLoader;
beforeEach(async () => { loader = new JsonFixtureLoader(dataSource); await loader.load('users'); await loader.load('posts'); });
it('should have loaded users', async () => { const users = await userRepository.find(); expect(users).toHaveLength(3); });});44.6 Faker Integration
Section titled “44.6 Faker Integration”Using Faker for Dynamic Data
Section titled “Using Faker for Dynamic Data”import { faker } from '@faker-js/faker';import { User } from '../../src/users/user.entity';import { Post } from '../../src/posts/post.entity';
export class FakerFixture { static createUser(overrides: Partial<User> = {}): User { const user = new User(); user.name = overrides.name || faker.person.fullName(); user.email = overrides.email || faker.internet.email(); user.password = overrides.password || faker.internet.password(); user.isActive = overrides.isActive ?? faker.datatype.boolean(); user.role = overrides.role || faker.helpers.arrayElement(['user', 'admin', 'moderator']); return user; }
static createUsers(count: number, overrides: Partial<User> = {}): User[] { return Array.from({ length: count }, () => this.createUser(overrides)); }
static createPost(overrides: Partial<Post> = {}): Post { const post = new Post(); post.title = overrides.title || faker.lorem.sentence(); post.content = overrides.content || faker.lorem.paragraphs(3); post.isPublished = overrides.isPublished ?? faker.datatype.boolean(); post.viewCount = overrides.viewCount || faker.number.int({ min: 0, max: 1000 }); return post; }
static createPosts(count: number, overrides: Partial<Post> = {}): Post[] { return Array.from({ length: count }, () => this.createPost(overrides)); }
static createPostWithAuthor(authorId?: number): { post: Post; author: User } { const author = this.createUser(); const post = this.createPost(); post.authorId = authorId; post.author = author; return { post, author }; }
static createBlogScenario(options: { userCount: number; postsPerUser: number; commentsPerPost: number; }) { const users = this.createUsers(options.userCount); const posts: Post[] = []; const comments: Comment[] = [];
users.forEach(user => { const userPosts = this.createPosts(options.postsPerUser); userPosts.forEach(post => { post.author = user; posts.push(post);
const postComments = Array.from({ length: options.commentsPerPost }, () => { const comment = new Comment(); comment.content = faker.lorem.paragraph(); comment.post = post; comment.author = users[faker.number.int({ min: 0, max: users.length - 1 })]; return comment; }); comments.push(...postComments); }); });
return { users, posts, comments }; }}
// Usagedescribe('With Faker', () => { it('should create realistic test data', async () => { const users = FakerFixture.createUsers(100); const saved = await userRepository.save(users);
expect(saved).toHaveLength(100); expect(saved[0].email).toMatch(/@/); // Valid email format });
it('should create blog scenario', async () => { const { users, posts, comments } = FakerFixture.createBlogScenario({ userCount: 10, postsPerUser: 3, commentsPerPost: 5, });
await userRepository.save(users); await postRepository.save(posts); await commentRepository.save(comments);
expect(posts).toHaveLength(30); // 10 users * 3 posts expect(comments).toHaveLength(150); // 30 posts * 5 comments });});44.7 Test State Management
Section titled “44.7 Test State Management”Test State Fixture
Section titled “Test State Fixture”import { DataSource } from 'typeorm';
export class TestState { private static instance: TestState; private data: Map<string, any> = new Map();
private constructor() {}
static getInstance(): TestState { if (!this.instance) { this.instance = new TestState(); } return this.instance; }
set<T>(key: string, value: T): void { this.data.set(key, value); }
get<T>(key: string): T | undefined { return this.data.get(key); }
clear(): void { this.data.clear(); }}
export class TestStateFixture { constructor(private dataSource: DataSource) {}
async setupAuthenticatedUser(): Promise<{ user: User; token: string }> { const state = TestState.getInstance();
// Check if already created let cached = state.get<{ user: User; token: string }>('authenticatedUser'); if (cached) { return cached; }
// Create new user const user = UserFixture.create({ email: 'authenticated@test.com', password: await bcrypt.hash('password123', 10), });
const saved = await this.dataSource.getRepository(User).save(user);
// Generate token (simplified) const token = 'generated-jwt-token';
const result = { user: saved, token }; state.set('authenticatedUser', result);
return result; }
async setupAdminUser(): Promise<User> { const state = TestState.getInstance();
let cached = state.get<User>('adminUser'); if (cached) { return cached; }
const admin = UserFixture.create({ role: 'admin' }); const saved = await this.dataSource.getRepository(User).save(admin);
state.set('adminUser', saved); return saved; }}44.8 Summary
Section titled “44.8 Summary” Test Fixtures Quick Reference +------------------------------------------------------------------+ | | | Fixture Type | Use Case | | -------------------|------------------------------------------| | Simple factory | Basic entity creation | | Builder pattern | Complex entity configuration | | JSON fixtures | Static, version-controlled data | | Faker fixtures | Dynamic, realistic data | | State fixtures | Shared test state | | | | Best Practices | Description | | -------------------|------------------------------------------| | Consistent data | Same fixture produces similar data | | Relationships | Support entity relationships | | Reset state | Clear counters between tests | | Reusable | Use across unit, integration, e2e | | Maintainable | Centralized fixture definitions | | | +------------------------------------------------------------------+Next Chapter
Section titled “Next Chapter”Chapter 45: Test Database Management
Last Updated: February 2026