You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在NestJS单元测试中Mock Knex?

解决NestJS中Knex Repository的单元测试Mock问题

当前测试的核心问题

  1. 错误的Knex Mock方式:你将KnexModule.forRoot()作为mock值传入,这是模块初始化配置,而非实际可调用的Knex实例
  2. 依赖注入Token错误:代码中拼写错误KnexConection(应为KnexConnection),且未模拟Knex的查询方法链(如knex('people').insert())
  3. 断言逻辑错误: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'
      );
    });
  });
});

关键说明

  1. 模拟Knex查询链:Knex采用链式调用,需模拟每个步骤的返回值,确保方法链能完整执行
  2. 匹配注入Token:使用Knex作为provide的token,匹配@InjectKnex()的默认注入标识(多连接场景需替换为对应连接token,如KNEX_CONNECTION)
  3. 覆盖异常场景:补充异常分支测试,验证错误处理逻辑的正确性
  4. 修正断言逻辑:根据createPerson的返回值类型,调整断言为匹配单个Person对象

内容的提问来源于stack exchange,提问作者Werter Pires Gouveia

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 05:35:56