如何在NestJS单元测试中Mock Knex?
解决NestJS中Knex Repository的单元测试Mock问题
当前测试的核心问题
- 错误的Knex Mock方式:你将
KnexModule.forRoot()作为mock值传入,这是模块初始化配置,而非实际可调用的Knex实例 - 依赖注入Token错误:代码中拼写错误
KnexConection(应为KnexConnection),且未模拟Knex的查询方法链(如knex('people').insert()) - 断言逻辑错误:
createPerson返回单个Person对象,而非数组,toBeInstanceOf(Array)不符合方法返回值类型
修正后的完整测试代码
import { Test, TestingModule } from '@nestjs/testing'; import { PeopleUtils } from '../people.utils'; import { InternalServerErrorException } from '@nestjs/common'; import { ICreatePersonData, Person } from '../types'; import { PeopleRepository } from '../people.repository'; import { ErrorsService } from 'src/shared/utils/errors.service'; import { Knex } from 'knex'; import { InjectKnex } from 'nestjs-knex'; // 定义测试用的模拟数据 const person1: Person = { id: 1, name: 'John', surname: 'Doe', person_type: 'INDIVIDUAL', cpf: '12345678901', birth_date: new Date('1990-01-01'), address: 'Main St', number: '100', city: 'New York', state: 'NY', zip_code: '10001', email: 'john@example.com', // 补充其他必填字段 }; const createPersonData1: ICreatePersonData = { name: 'John', surname: 'Doe', personType: 'INDIVIDUAL', cpf: '12345678901', birthDate: new Date('1990-01-01'), address: 'Main St', number: '100', city: 'New York', state: 'NY', zipCode: '10001', email: 'john@example.com', // 补充其他必填字段 }; describe('PeopleRepository', () => { let peopleRepository: PeopleRepository; let mockErrorsService: ErrorsService; let mockKnex: jest.Mocked<Knex>; beforeEach(async () => { // 模拟Knex实例及查询方法链 const mockKnexInstance = { // 模拟knex('people')返回查询构建器 (tableName: string) => { if (tableName === 'people') { return { // 模拟insert方法,返回包含id的数组 insert: jest.fn().mockResolvedValue([person1.id]), // 模拟findPersonById用到的where/first方法 where: jest.fn().mockReturnThis(), first: jest.fn().mockResolvedValue(person1), }; } return {}; }, } as unknown as jest.Mocked<Knex>; const module: TestingModule = await Test.createTestingModule({ providers: [ PeopleRepository, { provide: PeopleUtils, useValue: { createPeopleArrayFromDB: jest.fn().mockReturnValue([person1]), }, }, { provide: ErrorsService, useValue: { handleErrors: jest.fn().mockReturnValue(new InternalServerErrorException()), }, }, // 正确注入Mock的Knex实例,匹配@InjectKnex()的token { provide: Knex, useValue: mockKnexInstance, }, ], }).compile(); peopleRepository = module.get<PeopleRepository>(PeopleRepository); mockErrorsService = module.get<ErrorsService>(ErrorsService); mockKnex = module.get<Knex>(Knex) as jest.Mocked<Knex>; }); it('should be defined', () => { expect(peopleRepository).toBeDefined(); expect(mockErrorsService).toBeDefined(); expect(mockKnex).toBeDefined(); }); describe('createPerson', () => { it('should return a Person object on successful creation', async () => { // Arrange const peopleQueryBuilder = mockKnex('people') as any; // Act const result = await peopleRepository.createPerson(createPersonData1); // Assert expect(result).toBeDefined(); expect(result).toEqual(person1); // 验证insert方法被正确调用,传入转换后的参数 expect(peopleQueryBuilder.insert).toHaveBeenCalledWith({ name: createPersonData1.name, surname: createPersonData1.surname, person_type: createPersonData1.personType, cpf: createPersonData1.cpf, birth_date: createPersonData1.birthDate, address: createPersonData1.address, number: createPersonData1.number, city: createPersonData1.city, state: createPersonData1.state, zip_code: createPersonData1.zipCode, email: createPersonData1.email, // 补充其他映射后的字段 }); // 验证findPersonById的查询逻辑被触发 expect(peopleQueryBuilder.where).toHaveBeenCalledWith('id', person1.id); expect(peopleQueryBuilder.first).toHaveBeenCalled(); }); it('should throw error via ErrorsService when creation fails', async () => { // Arrange const peopleQueryBuilder = mockKnex('people') as any; const mockError = new Error('DB connection failed'); peopleQueryBuilder.insert.mockRejectedValue(mockError); // Act & Assert await expect(peopleRepository.createPerson(createPersonData1)).rejects.toThrow(InternalServerErrorException); expect(mockErrorsService.handleErrors).toHaveBeenCalledWith( mockError, '#Não foi possível criar a pessoa', 'repository/createPerson' ); }); }); });
关键说明
- 模拟Knex查询链:Knex采用链式调用,需模拟每个步骤的返回值,确保方法链能完整执行
- 匹配注入Token:使用
Knex作为provide的token,匹配@InjectKnex()的默认注入标识(多连接场景需替换为对应连接token,如KNEX_CONNECTION) - 覆盖异常场景:补充异常分支测试,验证错误处理逻辑的正确性
- 修正断言逻辑:根据
createPerson的返回值类型,调整断言为匹配单个Person对象
内容的提问来源于stack exchange,提问作者Werter Pires Gouveia
相关产品推荐
相关产品推荐

