How to unit test a custom repository of TypeORM in NestJS?
Asked Answered
W

2

5

Class to test

My TypeORM repository extends AbstractRepository:

@EntityRepository(User)
export class UsersRepository extends AbstractRepository<User> {

  async findByEmail(email: string): Promise<User> {
    return await this.repository.findOne({ email })
  }
}

Unit test

describe('UsersRepository', () => {
  let usersRepository: UsersRepository

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [UsersRepository]
    }).compile()

    usersRepository = module.get<UsersRepository>(UsersRepository)
  })

  describe('findByEmail', () => {
    it(`should return the user when the user exists in database.`, async () => {
      const fetchedUser = await usersRepository.findByEmail('[email protected]')
    })
  })
})

Here, I get the error:

TypeError: Cannot read property 'getRepository' of undefined

      at UsersRepository.get (repository/AbstractRepository.ts:43:29)
      at UsersRepository.findByEmail (users/users.repository.ts:11:23)
      at Object.<anonymous> (users/users.repository.spec.ts:55:49)

So, my question is, how do I mock the repository or repository.findOne?

In other words, how do I mock the fields that are inherited from the AbstractRepository which are protected and cannot be accessed from UsersRepository instance?

There is a similar question here but it is for extending from Repository<Entity> instead of AbstractRepository<Entity>. They are able to mock findOne because it's public.


What I tried

I tried to mock it in a NestJS recommended way, but this is for non-custom repositories and doesn't work in my case:

{
  provide: getRepositoryToken(User),
  useValue: {
    findOne: jest.fn().mockResolvedValue(new User())
  }
}
Wellworn answered 29/12, 2021 at 15:9 Comment(0)
W
8

I went with the in-memory database solution. This way I don't have to mock the complex queries of TypeORM. The unit tests run as fast without hitting the real database.

My production database is PostgreSQL, but I'm able to use SQLite in-memory database for unit testing. This works because the TypeORM provides an abtraction over databases. It doesn't matter what database we are using under the hood as long as we are satisfying the interface of our repository.

Here's how my tests look like:

const testConnection = 'testConnection'

describe('UsersRepository', () => {
  let usersRepository: UsersRepository

  beforeEach(async () => {
    const connection = await createConnection({
      type: 'sqlite',
      database: ':memory:',
      dropSchema: true,
      entities: [User],
      synchronize: true,
      logging: false,
      name: testConnection
    })

    usersRepository = connection.getCustomRepository(UsersRepository)
  })

  afterEach(async () => {
    await getConnection(testConnection).close()
  })

  describe('findByEmail', () => {
    it(`should return the user when the user exists in database.`, async () => {
      await usersRepository.createAndSave(testUser)
      const fetchedUser = await usersRepository.findByEmail(testUser.email)
      expect(fetchedUser.email).toEqual(testUser.email)
    })
  })
})
Wellworn answered 17/1, 2022 at 12:14 Comment(2)
Does it fit to the column type you declare for the entity? i am asking since i am using also postgres and one of the column is json type, also i have enum'sIsochronous
@MatanTubul, if you are using the TypeORM over the postgres, it should work although I haven't tested it. TypeORM queries eventually get converted to postgres or SQLite queries behind the scenes, depending on the database.Wellworn
N
0

Although your solution works, it causes repetition of db initialization code in all the repositories. I'd like to propose an alternate solution that works for me.

Step 1. Create a global configuration file jest.config.ts in your project root folder.

// project-root/jest.config.ts

import type { Config } from '@jest/types';

const config: Config.InitialOptions = {
  moduleFileExtensions: ['js', 'json', 'ts'],
  rootDir: 'src',
  testRegex: '.*\\.spec\\.ts$',
  testEnvironment: 'node',
  preset: 'ts-jest',
  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'], // [!code focus]
};

export default config;

Step 2. create global setup file src/jest.setup.ts that contains global initialization code.

// src/jest.setup.ts

import { TypeOrmModuleOptions } from '@nestjs/typeorm';

export const testDbConfig: TypeOrmModuleOptions = {
  type: 'sqlite',
  database: ':memory:',
  dropSchema: true,
  synchronize: true,
  autoLoadEntities: true,
};

// assign the testDbConfig to a global variable
global.testDbConfig = testDbConfig;

Step 3: Create your repositories and inject the entity manager into the constructor.

import { EntityManager, Repository } from 'typeorm';
import { User } from './entities/user.entity';

export class UserRepository extends Repository<User> {
  constructor(private readonly entityManager: EntityManager) {
    super(Movie, entityManager);
  }


  // my custom method that I need to unit test
  async findByEmail(title: string): Promise<User | undefined> {
    return this.findOne({ where: { email } });
  }
}

Step 4. Unit test your repositories by making entries into the in-memory database via the entityManager and then try reading the entries with your custom repository methods.

describe('UsersRepository', () => {
  let usersRepository: UsersRepository;
  let entityManager: EntityManager;

  beforeAll(async () => {
    const module: TestingModule = await Test.createTestingModule({
      imports: [TypeOrmModule.forRoot(global.testDbConfig), TypeOrmModule.forFeature([User])],
      providers: [
        {
          provide: UsersRepository,
          useFactory: (entityManager: EntityManager) => new UsersRepository(entityManager),
          inject: [EntityManager],
        },
      ],
    }).compile();

    const repositoryToken = getRepositoryToken(UsersRepository);
    usersRepository = module.get<UsersRepository>(repositoryToken);
    entityManager = module.get<EntityManager>(EntityManager);
  });

  // clear the user table to avoid clash with other test cases
  beforeEach(async () => {
    await entityManager.clear(User);
  });

  it('Should find the user by his email-id correctly', async () => {
    const testUser = new User();
    testUser.email = "[email protected]"

    await entityManager.save(User, testUser);

    const fetchedUser = await usersRepository.findByEmail(testUser.email);
    expect(fetchedUser).toBeDefined();
    expect(fetchedUser.email).toEqual(testUser.email)
  });
})

Once complete, just run the test suite. That's all!

Novelist answered 8/8 at 9:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.