angular - Cách để giả lập phụ thuộc/thuộc tính dịch vụ trạng thái ngxs riêng tư trong các bài kiểm tra đơn vị jest
Tôi đang sử dụng ngxs để quản lý trạng thái cho ứng dụng của mình.
@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();
// ...
}
}
Vấn đề
Tôi không hiểu làm thế nào tôi có thể sử dụng jest để giả lập phụ thuộc EmployeesService trong các bài kiểm tra của mình. Tài liệu liên quan đến việc kiểm thử cho NGXS cũng không cung cấp ví dụ nào.
Tôi chỉ mới bắt đầu với việc kiểm thử cho các ứng dụng angular/node nên tôi không có ý tưởng gì về những gì mình đang làm.
Tôi đã làm theo những gì tôi học được từ câu hỏi SO này và tôi đã thực hiện các bài kiểm tra sau.
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([]);
});
});
Điều này dẫn đến lỗi TypeError: provider.useFactory.apply is not a function khi tôi cố gắng chạy bài kiểm tra.
Hơn nữa, nơi tôi đặt giá trị cho employeesServiceStub trong hàm beforeEach, nó báo lỗi nói rằng giá trị tôi gán thiếu các thuộc tính còn lại từ EmployeesService thực tế của tôi. Về cơ bản, nó yêu cầu tôi thực hiện một mô phỏng đầy đủ (full mock) của dịch vụ. Điều này sẽ rất không hiệu quả đối với tôi vì trong mỗi bài kiểm tra, tôi sẽ cần phải định nghĩa một mô phỏng khác nhau cho các hàm khác nhau.
TS2740: Type '{ getEmployeeListQuery: Mock ; }' is missing the following properties from type 'EmployeesService': defaultHeaders, configuration, encoder, basePath, and 8 more.
Lý tưởng nhất, trong mỗi bài kiểm tra, tôi nên có khả năng định nghĩa các giá trị trả về khác nhau cho các hàm giả lập của EmployeesService trong từng bài kiểm tra, mà không cần phải định nghĩa các phiên bản giả lập của những hàm mà tôi không cần cho bài kiểm tra đó.
Vì các hàm trong EmployeesService là các hàm bất đồng bộ (async), tôi cũng không biết cách định nghĩa giá trị trả về bất đồng bộ cho các hàm đó. Tôi sẽ rất cảm kích nếu ai đó có thể giải thích rõ về vấn đề này.
Giải pháp cuối cùng
Dựa trên câu trả lời mà Mark Whitfield đưa ra, tôi đã thực hiện những thay đổi sau và điều đó đã giúp giải quyết vấn đề của tôi.
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([]);
});
});
Câu trả lời #1
Định nghĩa nhà cung cấp của bạn sử dụng useFactory là không đúng trong ví dụ của bạn. Bạn có thể thay đổi nó thành như sau:
providers: [
{ provide: EmployeesService, useFactory: () => employeesServiceStub }
]
Bạn có thể sử dụng useValue cho provider của mình, nhưng điều đó có nghĩa là bạn không thể gán lại mock mà bạn đã khởi tạo trong beforeEach, mà thay vào đó sẽ phải thay đổi nó:
providers: [
{ provide: EmployeesService, useValue: employeesServiceStub }
]
// then in your test...
employeesServiceStub..getEmployeeListQuery = jest.fn(....
Việc gán lại employeesServiceStub thực sự vẫn có thể là vấn đề cho bài kiểm tra của bạn, vì vậy bạn có thể thay đổi đối tượng đó thay vào, hoặc chuyển việc thiết lập TestBed vào trong bài kiểm tra của bạn.
Lưu ý: Việc giả lập các nhà cung cấp cho trạng thái NGXS giống như với bất kỳ dịch vụ Angular nào khác.
Về phần thứ hai của câu hỏi của bạn, nếu bạn ám chỉ một observable khi nói async (mà tôi có thể suy ra từ cách bạn sử dụng) thì bạn chỉ cần tạo một observable để trả về như một kết quả. Ví dụ:
import { of } from 'rxjs';
// ...
employeesServiceStub.getEmployeeListQuery = jest.fn((skip, take) => of([]))
PS. Nếu bạn thực sự muốn nói đến một Promise khi bạn nói async thì bạn chỉ cần đánh dấu phương thức của mình là async để nhận được một Promise làm kết quả. Ví dụ:
employeesServiceStub.getEmployeeListQuery = jest.fn(async (skip, take) => [])
