Search code examples
jestjsnestjstypeormnestjs-typeorm

How to mock `createQueryBuilder` in NestJS tests?


I have this NestJS service method which I need to test.

async getAllTests(orgId: string): Promise<Array<TestEntity>> {
  try {
    const query = this.repo.createQueryBuilder(this.tableName);
    query.where({ organization: orgId });
    const tests = await query.getMany();
    return tests;
  } catch (ex) {
    throw new InternalServerErrorException();
  }
}

I have this in my test file which is not really working, but I think it at least shows the direction I am trying to go.

let service: TestService;
let repo: Repository<TestEntity>;

beforeEach(async () => {
  const module: TestingModule = await Test.createTestingModule({
    providers: [
      TestService,
      {
        provide: getRepositoryToken(TestEntity),
        useValue: {
          create: jest.fn(),
          save: jest.fn(),
          delete: jest.fn(),
          createQueryBuilder: jest.fn(() => ({
            where: jest.fn().mockReturnThis(),
            getMany: jest.fn(),
            getOne: jest.fn(),
          })),
        },
      },
    ],
  }).compile();

  service = module.get<TestService>(TestService);
  repo = module.get<Repository<TestEntity>>(getRepositoryToken(TestEntity));
});
  
describe('getAllTests()', () => {
  it('should successfully return a list of tests', async () => {
    const qb = repo.createQueryBuilder();
    jest.spyOn(qb, 'getMany').mockResolvedValue([]);

    const result = service.getAllTests('foo');
    await expect(result).resolves.toEqual([]);
  });

  it('should throw an error when createQueryBuilder() fails', async () => {
    jest.spyOn(repo, 'createQueryBuilder').mockImplementation(() => {
      throw new Error();
    });

    const result = () => service.getAllTests('foo');
    await expect(result).rejects.toThrow(InternalServerErrorException);
  });

  it('should throw an error when createQueryBuilder().where fails', async () => {
    jest.spyOn(repo.createQueryBuilder(), 'where').mockImplementation(() => {
      throw new Error();
    });

    const result = () => service.getAllTests('foo');
    await expect(result).rejects.toThrow(InternalServerErrorException);
  });

  it('should throw an error when createQueryBuilder().getMany fails', async () => {
    jest.spyOn(repo.createQueryBuilder(), 'getMany').mockImplementation(() => {
      throw new Error();
    });

    const result = () => service.getAllTests('foo');
    await expect(result).rejects.toThrow(InternalServerErrorException);
  });
});

How do I mock either a return value or an error for createQueryBuilder() and it's methods? Right now the first test that should actually return data actually returns undefined, the one that directly mocks createQueryBuilder() works, but the others report this:

Received promise resolved instead of rejected
Resolved to value: undefined

Solution

  • The issue turns out to be that whenever repo.createQueryBuilder() is called it returns a new instance of that object. Thus whenever a sub-property is spied on with Jest, when the real method is called it will return a different instance than the one that was spied upon!

    Here's my solution. I've created a standalone file test-helpers.ts that contains this:

    export const mockRepositoryFactory = () => {
      const mockDeleteSingleton = jest.fn().mockReturnThis();
      const mockExecuteSingleton = jest.fn().mockReturnThis();
      const mockFromSingleton = jest.fn().mockReturnThis();
      const mockGetManySingleton = jest.fn().mockReturnThis();
      const mockGetOneSingleton = jest.fn().mockReturnThis();
      const mockInnerJoinSingleton = jest.fn().mockReturnThis();
      const mockInnerJoinAndSelectSingleton = jest.fn().mockReturnThis();
      const mockOrderBySingleton = jest.fn().mockReturnThis();
      const mockWhereSingleton = jest.fn().mockReturnThis();
    
      return {
        create: jest.fn(),
        save: jest.fn(),
        delete: jest.fn(),
        findOne: jest.fn(),
        findOneBy: jest.fn(),
        find: jest.fn(),
        createQueryBuilder: () => ({
          delete: mockDeleteSingleton,
          execute: mockExecuteSingleton,
          from: mockFromSingleton,
          getMany: mockGetManySingleton,
          getOne: mockGetOneSingleton,
          innerJoin: mockInnerJoinSingleton,
          innerJoinAndSelect: mockInnerJoinAndSelectSingleton,
          orderBy: mockOrderBySingleton,
          where: mockWhereSingleton,
        }),
      };
    };
    

    This will allow the exact same instance of those sub-properties to be returned when spyOn is called on them.

    So now, within my actual *.spec.ts file I simply use this imported mockRepository as my useFactory, which now will be fewer lines of code as well!

    import { mockRepositoryFactory} from '../testing-helpers';
    
    beforeEach(async () => {
      const module: TestingModule = await Test.createTestingModule({
        providers: [
          TestService,
          {
            provide: getRepositoryToken(TestEntity),
            useFactory: mockRepositoryFactory,  //<--- use it here
          },
        ],
      }).compile();
      
      service = module.get<TestService>(TestService);
      repo = module.get<Repository<TestEntity>>(getRepositoryToken(TestEntity));
    });
    

    Note: I originally had a static object and used useValue, which did work, except for the cases in which a test needed multiple repositories. Switching to use a factory instead fixed this.

    I should also note that I highly recommend turning on the resetMocks configuration option, which I cannot believe is not turned on by default. Jest is crazy sometimes.