I have a NestJS service that has a pretty simple method I am trying to write unit tests for
@Injectable()
export class TestService {
constructor(
@InjectRepository(TestEntity)
private readonly repo: Repository<TestEntity>,
) {}
async getAllTests(acct: AccountEntity): Promise<Array<TestEntity>> {
try {
const query = this.repo.createQueryBuilder(DB_TABLE_NAME_TEST);
query.where({ account: acct.id });
return await query.getMany();
} catch (ex) {
throw new InternalServerErrorException();
}
}
}
And the tests
const mockAccount: AccountEntity = {
id: 'mock-account-id-1234',
email: '[email protected]',
};
const mockTest: TestEntity = {
id: '1234-5678-9012',
account: 'mock-account-id-1234',
description: 'Test Name',
testerName: 'Bob Jones',
testerEmail: '[email protected]',
};
describe('TestService', () => {
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(),
getMany: jest.fn(),
getOne: jest.fn(),
})),
},
},
],
}).compile();
service = module.get(TestService);
repo = module.get(getRepositoryToken(TestEntity));
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('getAllTests()', () => {
it('should successfully get a list of all tests for the provided account', async () => {
jest.spyOn(repo.createQueryBuilder(DB_TABLE_NAME_TEST), 'where')
jest.spyOn(repo.createQueryBuilder(DB_TABLE_NAME_TEST), 'getMany').mockResolvedValue([mockTest]);
expect(repo.createQueryBuilder(DB_TABLE_NAME_TEST).where).toHaveBeenCalledWith({ account: mockAccount.id });
await expect(service.getAllTests(mockAccount)).resolves.toEqual([mockTest]);
});
it('should throw an error when the query fails', async () => {
jest.spyOn(repo.createQueryBuilder(DB_TABLE_NAME_TEST), 'where').mockImplementation(() => {
throw new Error();
});
jest.spyOn(repo.createQueryBuilder(DB_TABLE_NAME_TEST), 'getMany').mockImplementation(() => {
throw new Error();
});
const result = () => service.getAllTests(mockAccount);
await expect(result).rejects.toThrow(InternalServerErrorException);
});
});
});
Both of those tests for the getAllTests()
method fails with:
FAIL src/test/test.service.spec.ts
● TestService › getAllTests() › should successfully get a list of all tests for the provided account
expect(jest.fn()).toHaveBeenCalledWith(...expected)
Expected: {"account": "mock-account-id-1234"}
Number of calls: 0
143 | jest.spyOn(repo.createQueryBuilder(DB_TABLE_NAME_TEST), 'getMany').mockResolvedValue([mockTest]);
144 |
> 145 | expect(repo.createQueryBuilder(DB_TABLE_NAME_TEST).where).toHaveBeenCalledWith({ account: mockAccount.id });
| ^
146 | await expect(service.getAllTests(mockAccount)).resolves.toEqual([mockTest]);
147 | });
148 |
at Object.<anonymous> (test/test.service.spec.ts:145:65)
● TestService › getAllTests() › should throw an error when the query fails
expect(received).rejects.toThrow()
Received promise resolved instead of rejected
Resolved to value: undefined
157 |
158 | const result = () => service.getAllTests(mockAccount);
> 159 | await expect(result).rejects.toThrow(InternalServerErrorException);
| ^
160 | });
161 | });
162 | });
at expect (../node_modules/expect/build/index.js:113:15)
at Object.<anonymous> (test/test.service.spec.ts:159:13)
This seems like it should be pretty straight forward. What am I doing wrong here?
In your first test, you assert the mock.where to have been called before calling the service. Also every time you call createQueryBuilder
, you'll get back a new .where
with a new jest.fn()
tracking the calls to it. What I would suggest you do here is create a separate const whereMock = jest.fn().mockReturnThis()
so you can set where: whereMock
in the createQueryBuilder
object. This will allow you to access the correct jest.fn()
when asserting what the where
has been called with
And in the second test, once again because the createQueryBuilder
is a jest.fn()
every call to it returns a new object, so your jest.spyOn
calls are not spying on the same object that the service is using. To get past this, you can create mocks like I suggested for the whereMock
so they can be referenced directly without having to go through the new object.