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