简体   繁体   中英

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

I'm using ngxs to manage state for my app.

@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();
    // ...
  }
}

Problem

I don't understand how I can use jest to mock the EmployeesService dependency in my tests. The documentation related to testing for NGXS doesn't provide any examples either.

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

I followed what I learned from this SO question and I made the following tests.

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([]);
  });
});

This results in error TypeError: provider.useFactory.apply is not a function when I try to run the test.

Furthermore, where I set the value for the employeesServiceStub in the beforeEach function, it throws an error saying that the value I've assigned is missing the remaining properties from my actual EmployeesService . Essentially asking me to do a full mock implementation of the service. This would be very inefficient for me to do because, in each test, I'd need to define a different mocked implementation for different functions.

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

Ideally, in each test, I should be able to define different return values for the mocked functions of my EmployeesService within each test, without having to define mocked versions of the functions I don't need for that test.

Since the functions in EmployeesService are async functions, I have no idea how to define async return values for the functions either. I would really appreciate it if someone could shed some light on that.

Final solution

Based on the answer given by Mark Whitfield , I made the following changes that resulted in resolving my problem.

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([]);
  });
});

Your provider definition using useFactory is incorrect in your example. You could change it to this:

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

You could possibly use useValue for your provider, but that would mean that you could not reassign the mock that you initialized in your beforeEach , but would have to mutate it instead:

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

The reassign of employeesServiceStub may actually still be an issue for your test, so you could mutate the object instead, or move the TestBed setup into your test.

Note: Mocking out the providers for an NGXS state is the same as any other Angular service.

Regarding the second part of your question, if you mean an observable when you say async (which I can infer from your usage) then you can just create an observable to return as a result. For example:

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

PS. If you did mean a promise when you say async then you can just mark your method as async in order to get a promise as a result. For example:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM