如何在 jest 单元测试中模拟私有 ngxs 状态服务 dependency/property

How to mock private ngxs state service dependency/property in jest unit tests

我正在使用 ngxs 来管理我的应用程序的状态。

@State<EmployeesStateModel>({
  name: 'employees',
  defaults: {
    // ...
  }
})
@Injectable({
  providedIn: 'root'
})
export class EmployeesState {
  constructor(private employeesService: EmployeesService) {
  }

  @Action(GetEmployeesList)
  async getEmployeesList(ctx: StateContext<EmployeesStateModel>, action: GetEmployeesList) {

    const result = await this.employeesService
      .getEmployeeListQuery(0, 10).toPromise();
    // ...
  }
}

问题

我不明白如何在我的测试中使用 jest 来模拟 EmployeesService 依赖。与 NGXS 测试相关的文档也没有提供任何示例。

I'm just getting started with testing for angular/node applications so I have no idea what I'm doing.

我按照从 中学到的知识进行了以下测试。

describe('EmployeesStateService', () => {
  let store: Store;
  let employeesServiceStub = {} as EmployeesService;

  beforeEach(() => {
    employeesServiceStub = {
      getEmployeeListQuery: jest.fn()
    };
    TestBed.configureTestingModule({
      imports: [
        HttpClientTestingModule,
        NgxsModule.forRoot([EmployeesState])
      ],
      providers: [

        { provide: EmployeesService, useFactory: employeesServiceStub }
      ]
    });
    store = TestBed.inject(Store);
    TestBed.inject(EmployeesService);
  });

  it('gets a list of employees', async () => {
    employeesServiceStub = {
      getEmployeeListQuery: jest.fn((skip, take) => [])
    };

    await store.dispatch(new GetEmployeesList()).toPromise();

    const list = store.selectSnapshot(state => state.employees.employeesList);
    expect(list).toStrictEqual([]);
  });
});

当我尝试 运行 测试时,这会导致错误 TypeError: provider.useFactory.apply is not a function

此外,当我在 beforeEach 函数中设置 employeesServiceStub 的值时,它会抛出一个错误,指出我分配的值缺少实际 [=] 中的剩余属性15=]。本质上是要求我对该服务进行完整的模拟实施。这对我来说效率很低,因为在每次测试中,我都需要为不同的功能定义不同的模拟实现。

TS2740: Type '{ getEmployeeListQuery: Mock ; }' is missing the following properties from type 'EmployeesService': defaultHeaders, configuration, encoder, basePath, and 8 more.

理想情况下,在每个测试中,我应该能够在每个测试中为我的 EmployeesService 的模拟函数定义不同的 return 值,而不必定义我不使用的函数的模拟版本不需要那个测试。

由于 EmployeesService 中的函数是异步函数,我也不知道如何为函数定义异步 return 值。如果有人能对此有所了解,我将不胜感激。

最终解决方案

基于 ,我进行了以下更改,从而解决了我的问题。

describe('EmployeesStateService', () => {
  let store: Store;

  // Stub function response object that I will mutate in different tests.
  let queryResponse: QueryResponseDto = {};

  let employeesServiceStub = {
    // Ensure that the stubbed function returns the mutatable object.
    // NOTE: This function is supposed to be an async function, so 
    // the queryResponse object must be returned by the of() function 
    // which is part of rxjs. If your function is not supposed to be async
    // then no need to pass it to the of() function from rxjs here.
    // Thank you again Mark!
    getEmployeesListQuery: jest.fn((skip, take) => of(queryResponse))
  };

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        HttpClientTestingModule,
        NgxsModule.forRoot([EmployeesState])
      ],
      providers: [
        // Correctly use the useFactory option.
        { provide: EmployeesService, useFactory: () => employeesServiceStub }
      ]
    });
    store = TestBed.inject(Store);
    TestBed.inject(EmployeesService);
  });

  it('gets a list of employees', async () => {
    // Here I mutate the response object that the stubbed service will return
    queryResponse = {
      // ...
    };

    await store.dispatch(new GetEmployeesList()).toPromise();

    const list = store.selectSnapshot(state => state.employees.employeesList);
    expect(list).toStrictEqual([]);
  });
});

您使用 useFactory 的提供者定义在您的示例中不正确。 您可以将其更改为:

providers: [
  { provide: EmployeesService, useFactory: () => employeesServiceStub }
]

您可以使用 useValue 作为您的提供者,但这意味着您无法重新分配您在 beforeEach 中初始化的模拟,而是必须对其进行变异:

providers: [
  { provide: EmployeesService, useValue: employeesServiceStub }
]
// then in your test...
employeesServiceStub..getEmployeeListQuery = jest.fn(....

employeesServiceStub 的重新分配实际上可能仍然是您的测试的问题,因此您可以改变对象,或者将 TestBed 设置移到您的测试中。

注意:为 NGXS 状态模拟提供程序与任何其他 Angular 服务相同。

关于你问题的第二部分,如果你说异步时指的是一个可观察对象(我可以从你的用法中推断出来),那么你可以创建一个可观察到 return 作为结果。例如:

import { of } from 'rxjs';
// ...
employeesServiceStub.getEmployeeListQuery = jest.fn((skip, take) => of([]))

PS。如果您在说异步时确实是指承诺,那么您只需将方法标记为 async 即可获得承诺。例如:

employeesServiceStub.getEmployeeListQuery = jest.fn(async (skip, take) => [])