Search code examples
node.jstypescriptjestjsnestjstypeorm

How to mock chained function calls using jest?


I am testing the following service:

@Injectable()
export class TripService {
  private readonly logger = new Logger('TripService');

  constructor(
    @InjectRepository(TripEntity)
    private tripRepository: Repository<TripEntity>
  ) {}

  public async showTrip(clientId: string, tripId: string): Promise<Partial<TripEntity>> {
    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: () => MockType<Repository<any>> = jest.fn(() => ({
    findOne: jest.fn(entity => entity),
    findAndCount: jest.fn(entity => entity),
    create: jest.fn(entity => entity),
    save: jest.fn(entity => entity),
    update: jest.fn(entity => entity),
    delete: jest.fn(entity => entity),
    createQueryBuilder: jest.fn(() => ({
        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', () => {
  let service: TripService;
  let tripRepositoryMock: MockType<Repository<TripEntity>>;
  let driverRepositoryMock: MockType<Repository<DriverEntity>>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        TripService,
        { provide: getRepositoryToken(DriverEntity), useFactory: repositoryMockFactory },
        { provide: getRepositoryToken(TripEntity), useFactory: repositoryMockFactory },
      ],
    }).compile();

    service = module.get<TripService>(TripService);
    driverRepositoryMock = module.get(getRepositoryToken(DriverEntity));
    tripRepositoryMock = module.get(getRepositoryToken(TripEntity));
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
    expect(driverRepositoryMock).toBeDefined();
    expect(tripRepositoryMock).toBeDefined();
  });

  describe('TripService.showTrip()', () => {
    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 () => {
      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?


Solution

  • 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>();
    
    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 () => {
      const PARAMS = { contentId: '1', source: 'anything' };
    
      const FILTERED_REACTIONS = REACTIONS.filter(
        r => r.contentId === PARAMS.contentId && 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: () => createQueryBuilder,
        addSelect: () => createQueryBuilder,
        groupBy: () => createQueryBuilder,
        where: () => createQueryBuilder,
        getRawMany: () => FILTERED_REACTIONS,
      };
    
      jest
        .spyOn(reactionEntity, 'createQueryBuilder')
        .mockImplementation(() => createQueryBuilder);
    
      await expect(query.getContentReactions(PARAMS)).resolves.toEqual(
        FILTERED_REACTIONS,
      );
    });