Tools
Tools: Stop Writing Boilerplate: Test Data Generation for TypeORM That Actually Works
2026-01-21
0 views
admin
The Factory Pattern to the Rescue ## Getting Started (The Basics) ## Overriding Fields When You Need To ## Creating Multiple Entities ## The Game Changers: Advanced Features ## 1. Sequences for Unique Values ## 2. States for Different Scenarios ## 3. Lifecycle Hooks for Complex Setup ## 4. Associations for Related Entities ## 5. Async Factory Functions ## Real-World Example ## Integration with NestJS ## Build Method for Simple Mocks ## Testing Tips ## Conclusion We've all been there. You're writing tests for your NestJS application, and before you can even test the actual business logic, you need to create a bunch of entity instances. So you write something like this: Now multiply this by 50 tests, and you're looking at hundreds of lines of repetitive setup code. Not to mention when your entity schema changes, you'll need to update every single test. There has to be a better way, right? The factory pattern is well-established in the Rails community (FactoryBot) and Laravel (Factory classes), but it's been surprisingly underutilized in the TypeScript ecosystem. That's what led me to create and recently modernize typeorm-factories — a library that brings the power of test data factories to NestJS and TypeORM applications. Let me show you how it transforms your testing workflow. First, install the package: Now, instead of manually creating entities, you define a factory once: And use it in your tests: That's it. One line instead of eight. But the real magic happens when you need variations. Sometimes you need specific values for your test: The factory generates all the required fields with realistic data, but you can override any field you care about for your specific test case. Need test data in bulk? No problem: You can even override fields for all created entities: This is where things get interesting. In version 2.0, I added several features that solve real problems I've encountered in production codebases. Ever needed guaranteed unique emails or usernames in your tests? Sequences solve this elegantly: The sequence counter auto-increments for each entity. Clean, predictable, and no duplicate key errors in your tests. Let's say you have different user types in your app. Instead of creating separate factories or remembering which fields to override, define states: Now creating test users is expressive and readable: Your test clearly communicates what kind of user it's working with. What if you need to hash passwords or generate computed fields? Lifecycle hooks have you covered: This is especially useful when your entity has computed fields or needs specific transformations that you don't want to repeat in every test. Here's a real pain point: creating entities with relationships. Normally you'd need to create a user, then create posts, then link them together. With associations: The factory automatically creates all the related entities. This turns what could be 10+ lines of setup code into a single method call. Sometimes you need to do async operations during entity creation — maybe fetching data from an external service or performing database lookups: This works seamlessly with all other features — states, hooks, associations, everything. Let me show you how this all comes together in a realistic scenario. Imagine you're testing a blog platform: Notice how expressive the tests are. You can immediately understand what's being tested without wading through entity creation code. Since this is designed for NestJS applications, integration is straightforward: The FactoryModule automatically discovers and loads your factory definitions, so you don't need to manually import them in every test file. Sometimes you don't need Faker at all — you just want a simple object with specific values. The build() method is perfect for this: This creates a plain object without running faker or any hooks. It's faster and more predictable for simple mock scenarios. Here are some patterns I've found helpful: Reset sequences between tests to ensure predictable data: Use states to make tests self-documenting: Combine with Jest mocks for isolated unit tests: Testing shouldn't feel like a chore. With the right tools, it can actually be enjoyable (or at least less painful). typeorm-factories eliminates the boilerplate and lets you focus on what matters: writing good tests for your business logic. If you're tired of copying and pasting entity creation code across your test suite, give it a try: Check out the GitHub repository for full documentation and examples. What's your approach to test data generation? Do you use factories, builders, or something else? Let me know in the comments! Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK:
it('should update user profile', async () => { const user = new User(); user.id = 'some-uuid'; user.email = '[email protected]'; user.name = 'John Doe'; user.role = 'user'; user.status = 'active'; user.createdAt = new Date(); user.updatedAt = new Date(); // Finally, the actual test logic... const result = await service.updateProfile(user.id, { name: 'Jane Doe' }); expect(result.name).toBe('Jane Doe');
}); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
it('should update user profile', async () => { const user = new User(); user.id = 'some-uuid'; user.email = '[email protected]'; user.name = 'John Doe'; user.role = 'user'; user.status = 'active'; user.createdAt = new Date(); user.updatedAt = new Date(); // Finally, the actual test logic... const result = await service.updateProfile(user.id, { name: 'Jane Doe' }); expect(result.name).toBe('Jane Doe');
}); COMMAND_BLOCK:
it('should update user profile', async () => { const user = new User(); user.id = 'some-uuid'; user.email = '[email protected]'; user.name = 'John Doe'; user.role = 'user'; user.status = 'active'; user.createdAt = new Date(); user.updatedAt = new Date(); // Finally, the actual test logic... const result = await service.updateProfile(user.id, { name: 'Jane Doe' }); expect(result.name).toBe('Jane Doe');
}); CODE_BLOCK:
pnpm add -D typeorm-factories @faker-js/faker Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
pnpm add -D typeorm-factories @faker-js/faker CODE_BLOCK:
pnpm add -D typeorm-factories @faker-js/faker COMMAND_BLOCK:
// factories/user.factory.ts
import { faker } from '@faker-js/faker';
import { define } from 'typeorm-factories';
import { User } from '../entities/user.entity'; define(User, (faker) => { const user = new User(); user.id = faker.string.uuid(); user.email = faker.internet.email(); user.name = faker.person.fullName(); user.role = 'user'; user.status = 'active'; return user;
}); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
// factories/user.factory.ts
import { faker } from '@faker-js/faker';
import { define } from 'typeorm-factories';
import { User } from '../entities/user.entity'; define(User, (faker) => { const user = new User(); user.id = faker.string.uuid(); user.email = faker.internet.email(); user.name = faker.person.fullName(); user.role = 'user'; user.status = 'active'; return user;
}); COMMAND_BLOCK:
// factories/user.factory.ts
import { faker } from '@faker-js/faker';
import { define } from 'typeorm-factories';
import { User } from '../entities/user.entity'; define(User, (faker) => { const user = new User(); user.id = faker.string.uuid(); user.email = faker.internet.email(); user.name = faker.person.fullName(); user.role = 'user'; user.status = 'active'; return user;
}); COMMAND_BLOCK:
import { factory } from 'typeorm-factories'; it('should update user profile', async () => { const user = await factory(User).make(); const result = await service.updateProfile(user.id, { name: 'Jane Doe' }); expect(result.name).toBe('Jane Doe');
}); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
import { factory } from 'typeorm-factories'; it('should update user profile', async () => { const user = await factory(User).make(); const result = await service.updateProfile(user.id, { name: 'Jane Doe' }); expect(result.name).toBe('Jane Doe');
}); COMMAND_BLOCK:
import { factory } from 'typeorm-factories'; it('should update user profile', async () => { const user = await factory(User).make(); const result = await service.updateProfile(user.id, { name: 'Jane Doe' }); expect(result.name).toBe('Jane Doe');
}); COMMAND_BLOCK:
it('should not allow suspended users to post', async () => { const suspendedUser = await factory(User).make({ status: 'suspended' }); await expect(service.createPost(suspendedUser.id, postData)) .rejects .toThrow('User is suspended');
}); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
it('should not allow suspended users to post', async () => { const suspendedUser = await factory(User).make({ status: 'suspended' }); await expect(service.createPost(suspendedUser.id, postData)) .rejects .toThrow('User is suspended');
}); COMMAND_BLOCK:
it('should not allow suspended users to post', async () => { const suspendedUser = await factory(User).make({ status: 'suspended' }); await expect(service.createPost(suspendedUser.id, postData)) .rejects .toThrow('User is suspended');
}); COMMAND_BLOCK:
it('should return paginated user list', async () => { await factory(User).makeMany(25); const page1 = await service.getUsers({ page: 1, limit: 10 }); const page2 = await service.getUsers({ page: 2, limit: 10 }); expect(page1.items).toHaveLength(10); expect(page2.items).toHaveLength(10);
}); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
it('should return paginated user list', async () => { await factory(User).makeMany(25); const page1 = await service.getUsers({ page: 1, limit: 10 }); const page2 = await service.getUsers({ page: 2, limit: 10 }); expect(page1.items).toHaveLength(10); expect(page2.items).toHaveLength(10);
}); COMMAND_BLOCK:
it('should return paginated user list', async () => { await factory(User).makeMany(25); const page1 = await service.getUsers({ page: 1, limit: 10 }); const page2 = await service.getUsers({ page: 2, limit: 10 }); expect(page1.items).toHaveLength(10); expect(page2.items).toHaveLength(10);
}); CODE_BLOCK:
const admins = await factory(User).makeMany(5, { role: 'admin' }); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
const admins = await factory(User).makeMany(5, { role: 'admin' }); CODE_BLOCK:
const admins = await factory(User).makeMany(5, { role: 'admin' }); COMMAND_BLOCK:
define(User, (faker, settings, sequence) => { const user = new User(); user.email = `user${sequence}@test.com`; user.username = `user_${sequence}`; user.name = faker.person.fullName(); return user;
}); const users = await factory(User).makeMany(3);
// users[0].email = '[email protected]'
// users[1].email = '[email protected]'
// users[2].email = '[email protected]' Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
define(User, (faker, settings, sequence) => { const user = new User(); user.email = `user${sequence}@test.com`; user.username = `user_${sequence}`; user.name = faker.person.fullName(); return user;
}); const users = await factory(User).makeMany(3);
// users[0].email = '[email protected]'
// users[1].email = '[email protected]'
// users[2].email = '[email protected]' COMMAND_BLOCK:
define(User, (faker, settings, sequence) => { const user = new User(); user.email = `user${sequence}@test.com`; user.username = `user_${sequence}`; user.name = faker.person.fullName(); return user;
}); const users = await factory(User).makeMany(3);
// users[0].email = '[email protected]'
// users[1].email = '[email protected]'
// users[2].email = '[email protected]' COMMAND_BLOCK:
define(User, (faker) => { const user = new User(); user.email = faker.internet.email(); user.name = faker.person.fullName(); user.role = 'user'; user.emailVerified = false; return user;
}) .state('admin', (user) => { user.role = 'admin'; user.permissions = ['read', 'write', 'delete', 'admin']; return user; }) .state('verified', (user) => { user.emailVerified = true; user.emailVerifiedAt = new Date(); return user; }); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
define(User, (faker) => { const user = new User(); user.email = faker.internet.email(); user.name = faker.person.fullName(); user.role = 'user'; user.emailVerified = false; return user;
}) .state('admin', (user) => { user.role = 'admin'; user.permissions = ['read', 'write', 'delete', 'admin']; return user; }) .state('verified', (user) => { user.emailVerified = true; user.emailVerifiedAt = new Date(); return user; }); COMMAND_BLOCK:
define(User, (faker) => { const user = new User(); user.email = faker.internet.email(); user.name = faker.person.fullName(); user.role = 'user'; user.emailVerified = false; return user;
}) .state('admin', (user) => { user.role = 'admin'; user.permissions = ['read', 'write', 'delete', 'admin']; return user; }) .state('verified', (user) => { user.emailVerified = true; user.emailVerifiedAt = new Date(); return user; }); CODE_BLOCK:
const regularUser = await factory(User).make();
const admin = await factory(User).state('admin').make();
const verifiedAdmin = await factory(User).states(['admin', 'verified']).make(); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
const regularUser = await factory(User).make();
const admin = await factory(User).state('admin').make();
const verifiedAdmin = await factory(User).states(['admin', 'verified']).make(); CODE_BLOCK:
const regularUser = await factory(User).make();
const admin = await factory(User).state('admin').make();
const verifiedAdmin = await factory(User).states(['admin', 'verified']).make(); COMMAND_BLOCK:
import * as bcrypt from 'bcrypt'; define(User, (faker) => { const user = new User(); user.email = faker.internet.email(); user.password = 'password123'; // Plain password user.name = faker.person.fullName(); return user;
}) .beforeMake(async (user) => { // Hash password before entity is created user.password = await bcrypt.hash(user.password, 10); }) .afterMake(async (user) => { // Log creation for debugging console.log(`Created user: ${user.email}`); }); const user = await factory(User).make();
// Password is automatically hashed! Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
import * as bcrypt from 'bcrypt'; define(User, (faker) => { const user = new User(); user.email = faker.internet.email(); user.password = 'password123'; // Plain password user.name = faker.person.fullName(); return user;
}) .beforeMake(async (user) => { // Hash password before entity is created user.password = await bcrypt.hash(user.password, 10); }) .afterMake(async (user) => { // Log creation for debugging console.log(`Created user: ${user.email}`); }); const user = await factory(User).make();
// Password is automatically hashed! COMMAND_BLOCK:
import * as bcrypt from 'bcrypt'; define(User, (faker) => { const user = new User(); user.email = faker.internet.email(); user.password = 'password123'; // Plain password user.name = faker.person.fullName(); return user;
}) .beforeMake(async (user) => { // Hash password before entity is created user.password = await bcrypt.hash(user.password, 10); }) .afterMake(async (user) => { // Log creation for debugging console.log(`Created user: ${user.email}`); }); const user = await factory(User).make();
// Password is automatically hashed! COMMAND_BLOCK:
define(Post, (faker) => { const post = new Post(); post.title = faker.lorem.sentence(); post.content = faker.lorem.paragraphs(3); return post;
}) .association('author', User) .association('comments', Comment, { count: 5 }); const post = await factory(Post).make();
// post.author is a User instance
// post.comments is an array of 5 Comment instances Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
define(Post, (faker) => { const post = new Post(); post.title = faker.lorem.sentence(); post.content = faker.lorem.paragraphs(3); return post;
}) .association('author', User) .association('comments', Comment, { count: 5 }); const post = await factory(Post).make();
// post.author is a User instance
// post.comments is an array of 5 Comment instances COMMAND_BLOCK:
define(Post, (faker) => { const post = new Post(); post.title = faker.lorem.sentence(); post.content = faker.lorem.paragraphs(3); return post;
}) .association('author', User) .association('comments', Comment, { count: 5 }); const post = await factory(Post).make();
// post.author is a User instance
// post.comments is an array of 5 Comment instances COMMAND_BLOCK:
define(User, async (faker) => { const user = new User(); user.email = faker.internet.email(); user.name = faker.person.fullName(); // Simulate fetching avatar from external API user.avatarUrl = await fetchRandomAvatar(); return user;
}); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
define(User, async (faker) => { const user = new User(); user.email = faker.internet.email(); user.name = faker.person.fullName(); // Simulate fetching avatar from external API user.avatarUrl = await fetchRandomAvatar(); return user;
}); COMMAND_BLOCK:
define(User, async (faker) => { const user = new User(); user.email = faker.internet.email(); user.name = faker.person.fullName(); // Simulate fetching avatar from external API user.avatarUrl = await fetchRandomAvatar(); return user;
}); COMMAND_BLOCK:
// factories/user.factory.ts
define(User, (faker, settings, sequence) => { const user = new User(); user.id = sequence; user.email = `user${sequence}@blog.com`; user.username = faker.internet.userName(); user.name = faker.person.fullName(); user.bio = faker.lorem.paragraph(); user.role = 'author'; user.status = 'active'; return user;
}) .state('withPosts', async (user) => { user.posts = await factory(Post).makeMany(5, { authorId: user.id }); user.postCount = 5; return user; }) .state('featured', (user) => { user.featured = true; user.featuredAt = new Date(); return user; }) .beforeMake(async (user) => { user.slug = user.username.toLowerCase().replace(/[^a-z0-9]/g, '-'); }); // In your tests
describe('Blog Service', () => { beforeEach(() => { resetSequences(); // Start from 0 for each test }); it('should display featured authors on homepage', async () => { const featuredAuthors = await factory(User) .states(['withPosts', 'featured']) .makeMany(3); const homepage = await service.getHomepage(); expect(homepage.featuredAuthors).toHaveLength(3); expect(homepage.featuredAuthors[0].postCount).toBeGreaterThan(0); }); it('should only allow active authors to publish', async () => { const suspendedAuthor = await factory(User).make({ status: 'suspended' }); await expect(service.publishPost(suspendedAuthor.id, postData)) .rejects .toThrow('Author is suspended'); });
}); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
// factories/user.factory.ts
define(User, (faker, settings, sequence) => { const user = new User(); user.id = sequence; user.email = `user${sequence}@blog.com`; user.username = faker.internet.userName(); user.name = faker.person.fullName(); user.bio = faker.lorem.paragraph(); user.role = 'author'; user.status = 'active'; return user;
}) .state('withPosts', async (user) => { user.posts = await factory(Post).makeMany(5, { authorId: user.id }); user.postCount = 5; return user; }) .state('featured', (user) => { user.featured = true; user.featuredAt = new Date(); return user; }) .beforeMake(async (user) => { user.slug = user.username.toLowerCase().replace(/[^a-z0-9]/g, '-'); }); // In your tests
describe('Blog Service', () => { beforeEach(() => { resetSequences(); // Start from 0 for each test }); it('should display featured authors on homepage', async () => { const featuredAuthors = await factory(User) .states(['withPosts', 'featured']) .makeMany(3); const homepage = await service.getHomepage(); expect(homepage.featuredAuthors).toHaveLength(3); expect(homepage.featuredAuthors[0].postCount).toBeGreaterThan(0); }); it('should only allow active authors to publish', async () => { const suspendedAuthor = await factory(User).make({ status: 'suspended' }); await expect(service.publishPost(suspendedAuthor.id, postData)) .rejects .toThrow('Author is suspended'); });
}); COMMAND_BLOCK:
// factories/user.factory.ts
define(User, (faker, settings, sequence) => { const user = new User(); user.id = sequence; user.email = `user${sequence}@blog.com`; user.username = faker.internet.userName(); user.name = faker.person.fullName(); user.bio = faker.lorem.paragraph(); user.role = 'author'; user.status = 'active'; return user;
}) .state('withPosts', async (user) => { user.posts = await factory(Post).makeMany(5, { authorId: user.id }); user.postCount = 5; return user; }) .state('featured', (user) => { user.featured = true; user.featuredAt = new Date(); return user; }) .beforeMake(async (user) => { user.slug = user.username.toLowerCase().replace(/[^a-z0-9]/g, '-'); }); // In your tests
describe('Blog Service', () => { beforeEach(() => { resetSequences(); // Start from 0 for each test }); it('should display featured authors on homepage', async () => { const featuredAuthors = await factory(User) .states(['withPosts', 'featured']) .makeMany(3); const homepage = await service.getHomepage(); expect(homepage.featuredAuthors).toHaveLength(3); expect(homepage.featuredAuthors[0].postCount).toBeGreaterThan(0); }); it('should only allow active authors to publish', async () => { const suspendedAuthor = await factory(User).make({ status: 'suspended' }); await expect(service.publishPost(suspendedAuthor.id, postData)) .rejects .toThrow('Author is suspended'); });
}); COMMAND_BLOCK:
import { Test, TestingModule } from '@nestjs/testing';
import { FactoryModule } from 'typeorm-factories'; describe('UserService', () => { let service: UserService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [FactoryModule], // Import the module providers: [UserService, ...], }).compile(); await module.init(); // Important: Initialize the module service = module.get<UserService>(UserService); }); // Your tests...
}); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
import { Test, TestingModule } from '@nestjs/testing';
import { FactoryModule } from 'typeorm-factories'; describe('UserService', () => { let service: UserService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [FactoryModule], // Import the module providers: [UserService, ...], }).compile(); await module.init(); // Important: Initialize the module service = module.get<UserService>(UserService); }); // Your tests...
}); COMMAND_BLOCK:
import { Test, TestingModule } from '@nestjs/testing';
import { FactoryModule } from 'typeorm-factories'; describe('UserService', () => { let service: UserService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [FactoryModule], // Import the module providers: [UserService, ...], }).compile(); await module.init(); // Important: Initialize the module service = module.get<UserService>(UserService); }); // Your tests...
}); CODE_BLOCK:
const userMock = factory(User).build({ id: 1, email: '[email protected]', role: 'admin', name: 'Admin User'
}); jest.spyOn(repository, 'findOne').mockResolvedValue(userMock); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
const userMock = factory(User).build({ id: 1, email: '[email protected]', role: 'admin', name: 'Admin User'
}); jest.spyOn(repository, 'findOne').mockResolvedValue(userMock); CODE_BLOCK:
const userMock = factory(User).build({ id: 1, email: '[email protected]', role: 'admin', name: 'Admin User'
}); jest.spyOn(repository, 'findOne').mockResolvedValue(userMock); COMMAND_BLOCK:
beforeEach(() => { resetSequences();
}); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
beforeEach(() => { resetSequences();
}); COMMAND_BLOCK:
beforeEach(() => { resetSequences();
}); CODE_BLOCK:
// Instead of this:
const user = await factory(User).make({ role: 'admin', emailVerified: true, status: 'active'
}); // Do this:
const user = await factory(User).states(['admin', 'verified']).make(); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
// Instead of this:
const user = await factory(User).make({ role: 'admin', emailVerified: true, status: 'active'
}); // Do this:
const user = await factory(User).states(['admin', 'verified']).make(); CODE_BLOCK:
// Instead of this:
const user = await factory(User).make({ role: 'admin', emailVerified: true, status: 'active'
}); // Do this:
const user = await factory(User).states(['admin', 'verified']).make(); COMMAND_BLOCK:
it('should send email to new users', async () => { const user = await factory(User).make(); const emailSpy = jest.spyOn(emailService, 'send'); await service.createUser(user); expect(emailSpy).toHaveBeenCalledWith(user.email, expect.any(String));
}); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
it('should send email to new users', async () => { const user = await factory(User).make(); const emailSpy = jest.spyOn(emailService, 'send'); await service.createUser(user); expect(emailSpy).toHaveBeenCalledWith(user.email, expect.any(String));
}); COMMAND_BLOCK:
it('should send email to new users', async () => { const user = await factory(User).make(); const emailSpy = jest.spyOn(emailService, 'send'); await service.createUser(user); expect(emailSpy).toHaveBeenCalledWith(user.email, expect.any(String));
}); CODE_BLOCK:
pnpm add -D typeorm-factories @faker-js/faker Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
pnpm add -D typeorm-factories @faker-js/faker CODE_BLOCK:
pnpm add -D typeorm-factories @faker-js/faker
how-totutorialguidedev.toaimlssldatabasegitgithub