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

NestJS Service调用Repository的Mock测试失败,求排查解决

问题分析与修复

问题场景

我有一个NestJS的TestService,其中getAllTests方法通过Repository的QueryBuilder查询指定账户的TestEntity数据。为该方法编写了单元测试,但两个测试用例均失败,以下是相关代码及报错信息:

Service代码

@Injectable()
export class TestService {

  constructor(
    @InjectRepository(TestEntity)
    private readonly repo: Repository<TestEntity>,
  ) {}

  async getAllTests(acct: AccountEntity): Promise<Array<TestEntity>> {
    try {
      const query = this.repo.createQueryBuilder(DB_TABLE_NAME_TEST);
      query.where({ account: acct.id });

      return await query.getMany();
    } catch (ex) {
      throw new InternalServerErrorException();
    }
  }
}

原测试代码

const mockAccount: AccountEntity = {
  id: 'mock-account-id-1234',
  email: 'test@testerson.org',
};

const mockTest: TestEntity = {
  id: '1234-5678-9012',
  account: 'mock-account-id-1234',
  description: 'Test Name',
  testerName: 'Bob Jones',
  testerEmail: 'bob@aol.com',
};

describe('TestService', () => {
  let service: TestService;
  let repo: Repository<TestEntity>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        TestService,
        {
          provide: getRepositoryToken(TestEntity),
          useValue: {
            create: jest.fn(),
            save: jest.fn(),
            delete: jest.fn(),
            createQueryBuilder: jest.fn(() => ({
              where: jest.fn(),
              getMany: jest.fn(),
              getOne: jest.fn(),
            })),
          },
        },
      ],
    }).compile();

    service = module.get(TestService);
    repo = module.get(getRepositoryToken(TestEntity));
  });

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

  describe('getAllTests()', () => {
    it('should successfully get a list of all tests for the provided account', async () => {
      jest.spyOn(repo.createQueryBuilder(DB_TABLE_NAME_TEST), 'where')
      jest.spyOn(repo.createQueryBuilder(DB_TABLE_NAME_TEST), 'getMany').mockResolvedValue([mockTest]);

      expect(repo.createQueryBuilder(DB_TABLE_NAME_TEST).where).toHaveBeenCalledWith({ account: mockAccount.id });
      await expect(service.getAllTests(mockAccount)).resolves.toEqual([mockTest]);
    });

    it('should throw an error when the query fails', async () => {
      jest.spyOn(repo.createQueryBuilder(DB_TABLE_NAME_TEST), 'where').mockImplementation(() => {
        throw new Error();
      });

      jest.spyOn(repo.createQueryBuilder(DB_TABLE_NAME_TEST), 'getMany').mockImplementation(() => {
        throw new Error();
      });

      const result = () => service.getAllTests(mockAccount);
      await expect(result).rejects.toThrow(InternalServerErrorException);
    });
  });
});

原报错信息

FAIL  src/test/test.service.spec.ts
  ● TestService › getAllTests() › should successfully get a list of all tests for the provided account

    expect(jest.fn()).toHaveBeenCalledWith(...expected)

    Expected: {"account": "mock-account-id-1234"}

    Number of calls: 0

      143 |       jest.spyOn(repo.createQueryBuilder(DB_TABLE_NAME_TEST), 'getMany').mockResolvedValue([mockTest]);
      144 |
    > 145 |       expect(repo.createQueryBuilder(DB_TABLE_NAME_TEST).where).toHaveBeenCalledWith({ account: mockAccount.id });
          |                                                                 ^
      146 |       await expect(service.getAllTests(mockAccount)).resolves.toEqual([mockTest]);
      147 |     });
      148 |

      at Object.<anonymous> (test/test.service.spec.ts:145:65)

  ● TestService › getAllTests() › should throw an error when the query fails

    expect(received).rejects.toThrow()

    Received promise resolved instead of rejected
    Resolved to value: undefined

      157 |
      158 |       const result = () => service.getAllTests(mockAccount);
    > 159 |       await expect(result).rejects.toThrow(InternalServerErrorException);
          |             ^
      160 |     });
      161 |   });
      162 | });

      at expect (../node_modules/expect/build/index.js:113:15)
      at Object.<anonymous> (test/test.service.spec.ts:159:13)

错误原因

  1. QueryBuilder实例不统一:原测试中每次调用repo.createQueryBuilder()都会生成新的mock对象,导致测试代码中spy的实例和Service实际调用的实例不是同一个,因此断言where被调用时显示调用次数为0。
  2. 断言顺序错误:第一个测试中先断言where的调用,再执行Service方法,此时方法还未执行,自然没有调用记录。
  3. 链式调用未正确模拟:原QueryBuilder的mock中where方法没有返回this,不符合NestJS QueryBuilder的实际行为,可能导致调用链异常。
  4. 第二个测试的错误模拟未生效:同样因为实例不统一,模拟的getMany抛出错误没有被Service捕获,导致Promise没有被拒绝,反而返回undefined。

修复后的测试代码

const mockAccount: AccountEntity = {
  id: 'mock-account-id-1234',
  email: 'test@testerson.org',
};

const mockTest: TestEntity = {
  id: '1234-5678-9012',
  account: 'mock-account-id-1234',
  description: 'Test Name',
  testerName: 'Bob Jones',
  testerEmail: 'bob@aol.com',
};

describe('TestService', () => {
  let service: TestService;
  let repo: Repository<TestEntity>;
  let queryBuilderMock: any;

  beforeEach(async () => {
    // 提前定义QueryBuilder的mock实例,确保每次createQueryBuilder返回同一个对象
    queryBuilderMock = {
      where: jest.fn().mockReturnThis(),
      getMany: jest.fn(),
      getOne: jest.fn(),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        TestService,
        {
          provide: getRepositoryToken(TestEntity),
          useValue: {
            create: jest.fn(),
            save: jest.fn(),
            delete: jest.fn(),
            createQueryBuilder: jest.fn(() => queryBuilderMock),
          },
        },
      ],
    }).compile();

    service = module.get(TestService);
    repo = module.get(getRepositoryToken(TestEntity));
  });

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

  describe('getAllTests()', () => {
    it('should successfully get a list of all tests for the provided account', async () => {
      // 模拟getMany的返回值
      queryBuilderMock.getMany.mockResolvedValue([mockTest]);

      // 先执行Service方法
      const result = await service.getAllTests(mockAccount);

      // 再断言调用情况和返回值
      expect(repo.createQueryBuilder).toHaveBeenCalledWith(DB_TABLE_NAME_TEST);
      expect(queryBuilderMock.where).toHaveBeenCalledWith({ account: mockAccount.id });
      expect(result).toEqual([mockTest]);
    });

    it('should throw an error when the query fails', async () => {
      // 模拟getMany抛出错误
      queryBuilderMock.getMany.mockRejectedValue(new Error('Query failed'));

      // 断言抛出指定异常
      await expect(service.getAllTests(mockAccount)).rejects.toThrow(InternalServerErrorException);
    });
  });
});

修复要点

  • 提前定义QueryBuilder的mock实例,确保createQueryBuilder每次返回同一个对象,保证测试和Service使用的是同一个实例。
  • 调整断言顺序:先执行Service方法,再断言方法的调用情况和返回值。
  • 给where方法添加mockReturnThis(),符合QueryBuilder链式调用的实际行为。
  • 第二个测试中直接模拟getMany抛出错误,确保Service的try/catch能捕获并转换为InternalServerErrorException。

内容的提问来源于stack exchange,提问作者Chris Barr

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 09:41:01