DEV Community

Cover image for Stop Writing Boilerplate: Test Data Generation for TypeORM That Actually Works
barracode
barracode

Posted on

Stop Writing Boilerplate: Test Data Generation for TypeORM That Actually Works

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:

it('should update user profile', async () => {
  const user = new User();
  user.id = 'some-uuid';
  user.email = 'test@example.com';
  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

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 to the Rescue

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.

Getting Started (The Basics)

First, install the package:

pnpm add -D typeorm-factories @faker-js/faker
Enter fullscreen mode Exit fullscreen mode

Now, instead of manually creating entities, you define a factory once:

// 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

And use it in your tests:

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

That's it. One line instead of eight. But the real magic happens when you need variations.

Overriding Fields When You Need To

Sometimes you need specific values for your test:

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

The factory generates all the required fields with realistic data, but you can override any field you care about for your specific test case.

Creating Multiple Entities

Need test data in bulk? No problem:

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

You can even override fields for all created entities:

const admins = await factory(User).makeMany(5, { role: 'admin' });
Enter fullscreen mode Exit fullscreen mode

The Game Changers: Advanced Features

This is where things get interesting. In version 2.0, I added several features that solve real problems I've encountered in production codebases.

1. Sequences for Unique Values

Ever needed guaranteed unique emails or usernames in your tests? Sequences solve this elegantly:

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 = 'user0@test.com'
// users[1].email = 'user1@test.com'
// users[2].email = 'user2@test.com'
Enter fullscreen mode Exit fullscreen mode

The sequence counter auto-increments for each entity. Clean, predictable, and no duplicate key errors in your tests.

2. States for Different Scenarios

Let's say you have different user types in your app. Instead of creating separate factories or remembering which fields to override, define states:

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

Now creating test users is expressive and readable:

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

Your test clearly communicates what kind of user it's working with.

3. Lifecycle Hooks for Complex Setup

What if you need to hash passwords or generate computed fields? Lifecycle hooks have you covered:

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

This is especially useful when your entity has computed fields or needs specific transformations that you don't want to repeat in every test.

4. Associations for Related Entities

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:

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

The factory automatically creates all the related entities. This turns what could be 10+ lines of setup code into a single method call.

5. Async Factory Functions

Sometimes you need to do async operations during entity creation — maybe fetching data from an external service or performing database lookups:

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

This works seamlessly with all other features — states, hooks, associations, everything.

Real-World Example

Let me show you how this all comes together in a realistic scenario. Imagine you're testing a blog platform:

// 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

Notice how expressive the tests are. You can immediately understand what's being tested without wading through entity creation code.

Integration with NestJS

Since this is designed for NestJS applications, integration is straightforward:

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

The FactoryModule automatically discovers and loads your factory definitions, so you don't need to manually import them in every test file.

Build Method for Simple Mocks

Sometimes you don't need Faker at all — you just want a simple object with specific values. The build() method is perfect for this:

const userMock = factory(User).build({
  id: 1,
  email: 'admin@example.com',
  role: 'admin',
  name: 'Admin User'
});

jest.spyOn(repository, 'findOne').mockResolvedValue(userMock);
Enter fullscreen mode Exit fullscreen mode

This creates a plain object without running faker or any hooks. It's faster and more predictable for simple mock scenarios.

Testing Tips

Here are some patterns I've found helpful:

Reset sequences between tests to ensure predictable data:

beforeEach(() => {
  resetSequences();
});
Enter fullscreen mode Exit fullscreen mode

Use states to make tests self-documenting:

// 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

Combine with Jest mocks for isolated unit tests:

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

Conclusion

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:

pnpm add -D typeorm-factories @faker-js/faker
Enter fullscreen mode Exit fullscreen mode

Check out the GitHub repository for full documentation and examples.

Happy testing! 🎉


What's your approach to test data generation? Do you use factories, builders, or something else? Let me know in the comments!

Top comments (0)