如何使用 jest 模拟链式函数调用?

Posted

技术标签:

【中文标题】如何使用 jest 模拟链式函数调用?【英文标题】:How to mock chained function calls using jest? 【发布时间】:2019-10-31 20:05:27 【问题描述】:

我正在测试以下服务:

@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;
  

我的存储库模拟:

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(),
    )),
));

我的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);
    );
  );
);

我想模拟对tripRepository.createQueryBuilder().innerJoinAndSelect().where().select().getOne();的调用

第一个问题,我应该在这里模拟链式调用吗,因为我认为它应该已经在 Typeorm 中进行了测试。

其次,如果我想模拟传递给每个链式调用的参数,最后还要模拟返回值,我该怎么做呢?

【问题讨论】:

我会说最好只使用具有特定 NODE_ENV 和配置的测试数据库,以便您的测试包含所有的 sql 事务管理。在我看来,模拟 TypeOrm SQL 交互是测试服务的次优方法,而且在开发时间上也相当昂贵。 @zenbeni 我将在 e2e 测试中使用测试数据库进行测试。这只是一个单元测试,所以我不想使用数据库,因为它会减慢测试并增加与数据库的紧密耦合。 这项服务背后的意义是什么?它似乎是您的存储库的包装器。如果你想进行单元测试,你应该对你的业务逻辑而不是持久层进行单元测试。 TripRepository 是来自图书馆还是您拥有它?您的测试方法似乎更像是一种功能测试,您应该为其设置一个实际的测试数据库。 另外,我认为你不应该创建一个单元测试这个服务。该服务依赖于数据库。模拟整个数据库连接是没有意义的。相反,此服务是您对其他服务的依赖项,应该被嘲笑。对于其他服务,您应该编写测试:“假设我的 TripService 给了我一次旅行,那么我希望这会发生”和“假设我得到一个异常,我希望它以这种方式处理”您不应该模拟每个调用存储库。这是您的底层持久层。假设它正在工作。 @k0pernikus 这个服务不仅仅是一个包装器,它在查询前后都有业务逻辑。我只是举个例子。真正的问题是关于模拟链式函数调用。 【参考方案1】:

我有类似的需求并使用以下方法解决。

这是我试图测试的代码。注意createQueryBuilder 和我调用的所有嵌套方法。

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;

现在,看看我编写的模拟上述方法的链式调用的测试。

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,
  );
);

【讨论】:

这个。这个这个这个。关于这个主题有很多问题和答案,但这是适用于 Jest、TypeScript 和第三方库的交集的解决方案【参考方案2】:

Guilherme 的回答是完全正确的。我只是想提供一种可能适用于更多测试用例和 TypeScript 的修改方法。您可以使用jest.fn,而不是将您的链接调用定义为(),从而允许您进行更多断言。例如,

/* eslint-disable  @typescript-eslint/no-explicit-any */
const createQueryBuilder: any = 
  select: jest.fn().mockImplementation(() => 
    return createQueryBuilder
  ),
  addSelect: jest.fn().mockImplementation(() => 
    return createQueryBuilder
  ),
  groupBy: jest.fn().mockImplementation(() => 
    return createQueryBuilder
  ),
  where: jest.fn().mockImplementation(() => 
    return createQueryBuilder
  ),
  getRawMany: jest
    .fn()
    .mockImplementationOnce(() => 
      return FILTERED_REACTIONS
    )
    .mockImplementationOnce(() => 
      return SOMETHING_ELSE
    ),


/* run your code */

// then you can include an assertion like this:
expect(createQueryBuilder.groupBy).toHaveBeenCalledWith(`some group`)

【讨论】:

以上是关于如何使用 jest 模拟链式函数调用?的主要内容,如果未能解决你的问题,请参考以下文章

如何在每次测试之前重置 Jest 模拟函数调用计数

当模拟点击调用调用 promise 的函数时,使用 React 的 Jest 和 Enzyme 进行测试

如何在 Sinon 中模拟链式函数调用

Jest Vue 预期的模拟函数已被调用,但未调用

在其他函数中调用时,Jest模拟函数不起作用

Jest spyOn 函数调用