Title: How to mock chained function calls using jest?
Tags: node.js;typescript;jestjs;nestjs;typeorm
Question: I am testing the following service:
@Injectable()
export class TripService {
private readonly logger = new Logger('TripService');
constructor(
@InjectRepository(TripEntity)
private tripRepository: Repository<TripEntity&gt;
) {}
public async showTrip(clientId: string, tripId: string): Promise<Partial<TripEntity&gt;&gt; {
const trip = await this.tripRepository
.createQueryBuilder('trips')
.innerJoinAndSelect('trips.driver', 'driver', 'driver.clientId = :clientId', { clientId })
.where({ id: tripId })
.select([
'trips.id',
'trips.distance',
'trips.sourceAddress',
'trips.destinationAddress',
'trips.startTime',
'trips.endTime',
'trips.createdAt'
])
.getOne();
if (!trip) {
throw new HttpException('Trip not found', HttpStatus.NOT_FOUND);
return trip;
My repository mock:
export const repositoryMockFactory: () =&gt; MockType<Repository<any&gt;&gt; = jest.fn(() =&gt; ({
findOne: jest.fn(entity =&gt; entity),
findAndCount: jest.fn(entity =&gt; entity),
create: jest.fn(entity =&gt; entity),
save: jest.fn(entity =&gt; entity),
update: jest.fn(entity =&gt; entity),
delete: jest.fn(entity =&gt; entity),
createQueryBuilder: jest.fn(() =&gt; ({
delete: jest.fn().mockReturnThis(),
innerJoinAndSelect: jest.fn().mockReturnThis(),
innerJoin: jest.fn().mockReturnThis(),
from: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
execute: jest.fn().mockReturnThis(),
getOne: jest.fn().mockReturnThis(),
})),
My tripService.spec.ts:
import { Test, TestingModule } from '@nestjs/testing';
import { TripService } from './trip.service';
import { MockType } from '../mock/mock.type';
import { Repository } from 'typeorm';
import { TripEntity } from './trip.entity';
import { getRepositoryToken } from '@nestjs/typeorm';
import { repositoryMockFactory } from '../mock/repositoryMock.factory';
import { DriverEntity } from '../driver/driver.entity';
import { plainToClass } from 'class-transformer';
describe('TripService', () =&gt; {
let service: TripService;
let tripRepositoryMock: MockType<Repository<TripEntity&gt;&gt;;
let driverRepositoryMock: MockType<Repository<DriverEntity&gt;&gt;;
beforeEach(async () =&gt; {
const module: TestingModule = await Test.createTestingModule({
providers: [
TripService,
{ provide: getRepositoryToken(DriverEntity), useFactory: repositoryMockFactory },
{ provide: getRepositoryToken(TripEntity), useFactory: repositoryMockFactory },
],
}).compile();
service = module.get<TripService&gt;(TripService);
driverRepositoryMock = module.get(getRepositoryToken(DriverEntity));
tripRepositoryMock = module.get(getRepositoryToken(TripEntity));
it('should be defined', () =&gt; {
expect(service).toBeDefined();
expect(driverRepositoryMock).toBeDefined();
expect(tripRepositoryMock).toBeDefined();
describe('TripService.showTrip()', () =&gt; {
const trip: TripEntity = plainToClass(TripEntity, {
id: 'one',
distance: 123,
sourceAddress: 'one',
destinationAddress: 'one',
startTime: 'one',
endTime: 'one',
createdAt: 'one',
});
it('should show the trip is it exists', async () =&gt; {
tripRepositoryMock.createQueryBuilder.mockReturnValue(trip);
await expect(service.showTrip('one', 'one')).resolves.toEqual(trip);
});
I want to mock the call to the tripRepository.createQueryBuilder().innerJoinAndSelect().where().select().getOne();
First question, should I mock the chained calls here because I assume that it should already be tested in Typeorm.
Second, if I want to mock the parameters passed to each chained call and finally also mock the return value, how can I go about it?
Comment: I would say that it could be better that you just use a test database with specific NODE_ENV and configuration so that your test includes all the sql transaction management in it. Mocking TypeOrm SQL interactions seems to me like a suboptimal way to test your service that is also quite expensive in dev time.

Comment: What's the meaning behind this service? It appears to be a wrapper of your repository. If you want to unit test, you should rather unit test your business logic instead of the persistence layer. Is the TripRepository comming from a library or do you own it? Your test approach appears to be more a functional test and you should set up an actual test database for it.

Comment: Additionally, I'd argue that you should not create a unit test this service. This service depends on the database. It makes no sense to mock your entire database connection. Rather, this service is your dependency for other services and should be the one mocked. For other services, you should write tests: "Assume my TripService gives me a trip, then I expect this to happen" and "Assume I get an exception, I expect it to be handled that way" You should not mock each call on the repository. This is your underlying persistence layer. Assume it's working.

Comment: @AbhyuditJain That's the issue that your are mixing business logic inside your repository. Create a real facade for your repository. It should only contain methods accessing your tripReposiory. That facade should only provide methods such as `getOneTrip(clientId: string, tripId: string)`. Create another service, that only contains your business logic. That service should depend on your RepositoryFacade. You now only have to mock the `getOneTrip` call instead of mocking the chained methods.

Comment: Since you are using typeorm and nestjs, this may help you: https://github.com/BrunnerLivio/nestjs-integration-test-db-example

Comment: @zenbeni I will test with test database in e2e tests. This is just a unit test so I don't want to use database for it, as it will slow tests and add tight coupling with database.

Comment: @k0pernikus This service is not just a wrapper, this has business logic before and after the query. I just gave it as an example. The real question is about mocking chained function calls.

Comment: @k0pernikus I ended up doing that only. I extended Repository and made my custom functions.

Here is the accepted answer: I had a similar need and solved using the following approach.
This is the code I was trying to test. Pay attention to the createQueryBuilder and all the nested methods I called.
const reactions = await this.reactionEntity
.createQueryBuilder(TABLE_REACTIONS)
.select('reaction')
.addSelect('COUNT(1) as count')
.groupBy('content_id, source, reaction')
.where(`content_id = :contentId AND source = :source`, {
contentId,
source,
.getRawMany<GetContentReactionsResult&gt;();
return reactions;
Now, take a look at the test I wrote that simulates the chained calls of the above methods.
it('should return the reactions that match the supplied parameters', async () =&gt; {
const PARAMS = { contentId: '1', source: 'anything' };
const FILTERED_REACTIONS = REACTIONS.filter(
r =&gt; r.contentId === PARAMS.contentId &amp;&amp; r.source === PARAMS.source,
// Pay attention to this part. Here I created a createQueryBuilder
// const with all methods I call in the code above. Notice that I return
// the same `createQueryBuilder` in all the properties/methods it has
// except in the last one that is the one that return the data
// I want to check.
const createQueryBuilder: any = {
select: () =&gt; createQueryBuilder,
addSelect: () =&gt; createQueryBuilder,
groupBy: () =&gt; createQueryBuilder,
where: () =&gt; createQueryBuilder,
getRawMany: () =&gt; FILTERED_REACTIONS,
jest
.spyOn(reactionEntity, 'createQueryBuilder')
.mockImplementation(() =&gt; createQueryBuilder);
await expect(query.getContentReactions(PARAMS)).resolves.toEqual(
FILTERED_REACTIONS,
Comment for this answer: this implementation works with a few details for aggregate in TypeOrm
Comment for this answer: This. This this this. There are lots of questions and answers on this subject but this is the solution that works at the intersection of Jest, TypeScript, and third-party libraries

Here is another answer: Guilherme's answer is totally right. I just wanted to offer a modified approach that might apply to more test cases, and in TypeScript. Instead of defining your chained calls as (), you can use a jest.fn, allowing you to make more assertions. e.g.,
/* eslint-disable  @typescript-eslint/no-explicit-any */
const createQueryBuilder: any = {
select: jest.fn().mockImplementation(() =&gt; {
return createQueryBuilder
addSelect: jest.fn().mockImplementation(() =&gt; {
return createQueryBuilder
groupBy: jest.fn().mockImplementation(() =&gt; {
return createQueryBuilder
where: jest.fn().mockImplementation(() =&gt; {
return createQueryBuilder
getRawMany: jest
.fn()
.mockImplementationOnce(() =&gt; {
return FILTERED_REACTIONS
})
.mockImplementationOnce(() =&gt; {
return SOMETHING_ELSE
}),
/* run your code */
// then you can include an assertion like this:
expect(createQueryBuilder.groupBy).toHaveBeenCalledWith(`some group`)

Here is another answer: The solution I found to work in my case was to
create a repository class, add your custom query to the class
@EntityRepository(User)
export class UserRepository extends Repository<User&gt; {
async getStatus(id: string) {
const status = await this.createQueryBuilder()
.select('User.id')
.where('User.id = :id', { id })
.getRawOne();
return {status};
mock the new repository class using 'jest-mock-extended' and 'jest-when' dependencies. This way you only need to mock the UserRepository and not all it's nested queries.
Now you can define the behaviour of the repository to resolve a predefined object (in my case a Partial object).
// some file where I need to call getStatus() in a test
const userRepoMock = mock<UserRepository&gt;()
// lines omitted
const user = {
status: open,
when(userRepoMock.getStatus).mockResolvedValue(user as User);
// assert status

{'source': 'StackExchangeDataset', 'subset': 'stackoverflow.com', 'metadata': {'AnswerCount': 3, 'CommentCount': 8, 'ContentLicense': 'CC BY-SA 4.0', 'CreationDate': '2019:00:06/18/19', 'FavoriteCount': 5.0, 'PostTypeId': 1, 'Tags': '<node.js><typescript><jestjs><nestjs><typeorm>', 'Title': 'How to mock chained function calls using jest?', 'ViewCount': 8917, 'id': 56644690, 'user_AboutMe': '<p>Self taught programmer with a thirst to learn more and more about anything and everything.</p>\n', 'user_AccountId': 4454545.0, 'user_DisplayName': 'Abhyudit Jain', 'user_Reputation': 3380.0, 'user_Views': 283.0, 'user_id': 3743683.0}}