top of page

Unit Testing Node.js with tsyringe and TypeORM

Jan 31, 2023

3 min read

Get the code for this tutorial here: https://github.com/hypercolor/typeorm-unit-testing I've been using TypeORM for my Node.js database work for years, but until recently I wasn't writing units tests with it.  When using TypeORM's ActiveRecord pattern, testing without a database connection is difficult.  You can spin up a test database or even an in-memory sqlite database, but these are always expensive choices in terms of the time it takes to run the tests.  Ideally the unit test suite is fully isolated and blazing fast. The key to unit testing with TypeORM is to convert to the DataMapper pattern, as opposed to ActiveRecord.  This adds a bit of boilerplate as you will need to create a Repository for each model, but the payoff is that your models will no longer inherit from BaseEntity.  This inheritance is what makes unit testing with ActiveRecord so difficult - inheriting from BaseEntity will throw errors unless a database connection has been initialized.


@Entity()
export class User {
  @Column() public id!: number;
  @Column() public email!: string;
  @Column() public hashedPassword!: string;
}
Example of an entity with DataMapper pattern

Once you can actually use your model classes, it's just a matter of mocking dependencies in the code under test and writing those tests.  Dependency injection makes mocking easier by making it clear what actually needs to be mocked and providing a standard interface for injecting mocks. In the sample repository we show a test for a function LoginService.loginUser, which looks like this:


@injectable()
export class LoginService {

  constructor(
    @inject("SessionRepository") private sessionRepository: ISessionRepository,
    @inject("UserRepository") private userRepository: IUserRepository
  ) {}

  public async loginUser(email: string, passwordAttempt: string) {
    const user = await this.userRepository.findOne({where: {email}})
    if (!user) {
      throw new Error('User not found');
    }
    if (!bcrypt.compareSync(user.hashedPassword, passwordAttempt)) {
      throw new Error('Password mismatch');
    }
    const session = new Session();
    session.userId = user.id;
    session.token = crypto.randomBytes(48).toString('hex');
    await this.sessionRepository.save(session);
    return session;
  }
}
Login service logic that we want to test

To write a unit test for this code, we'll need to provide mocks for the sessionRepository and userRepository dependencies. We use a simple helper called RepositoryMock which is just a way to easily create objects that can be used in place of repositories. As the application and test suite grows, additional TypeORM actions will be added here, such as find or count:

export class RepositoryMock {
  public save = jest.fn();
  public findOne = jest.fn();
  public findOne_addFixture(value: any) {
    this.findOne.mockReturnValue(value);
  }
}
Pattern for mocking TypeORM Repositories

To use the mocks, we call a mock setup function before each test called RepositoryMocks.getMocks(). This creates the mocks and registers them with the Dependency Injection framework. Now when the service logic is run in the test, it will be set up with these mocks instead of live repositories.

export class RepositoryMocks {
  public static getMocks() {
    const sessionRepository = new RepositoryMock();
    const profileRepository = new RepositoryMock();
    container.register("SessionRepository", {useValue: sessionRepository});
    container.register("ProfileRepository", {useValue: profileRepository});
    return {
      sessionRepository,
      profileRepository
    };
  }
}
Registering the mocked repositories so they will be injected by tsyringe

When writing each unit test, I follow the same pattern each time to keep them concise and focused:

  1. Set up mocks and fixtures

  2. Instantiate code under test

  3. Call the function to be tested

  4. Assertions about results


With this framework, these tests can look like:

describe("getSessionForLogin", () => {
  it("session found", async () => {
    const mocks = RepositoryMocks.getMocks();
    const loginService = container.resolve(LoginService);
    mocks.userRepository.findOne_addFixture(Fixtures.User.user1);
    const session = await loginService.loginUser(Fixtures.User.user1.email, Fixtures.User.user1.password);
    expect(session).not.toBe(undefined);
    expect(mocks.userRepository.findOne.mock.calls.length).toBe(1);
    expect(mocks.sessionRepository.save.mock.calls.length).toBe(1);
  });
});
Sample test with mocked repositories and injected LoginService

I hope this helps you to start writing tests. Let me know if this works for you!


Related Posts

Comments

Share Your ThoughtsBe the first to write a comment.