#angular #unit-testing #rxjs #jestjs #combinelatest
#угловой #модульное тестирование #rxjs #jestjs #combinelatest
Вопрос:
Я уже давно начал с модульного тестирования с помощью фреймворка Jest в Angular. Однако я застрял в ситуации, когда мне нужно модульное тестирование combineLatest оператора RxJS. Мой компонент выглядит так, как показано ниже.
Компонент:
public userData;
public productData;
constructor(
private readonly userService: UserService,
private readonly productService: ProductService
) {}
public ngOnInit() {
this.initialize();
}
public initialize() {
combineLatest([
this.userService.getUserData(),
this.productService.getProductData()
])
.subscribe([userData, productData] => {
this.userData = userData;
this.productData = productData;
});
}
Я высмеял оба своих сервиса, и мой модульный тест выглядит так, как показано ниже.
it('should initialize user and product data', fakeAsync(() => {
spyOn(userService, 'getUserData');
spyOn(productService, 'getProductData');
component.initialize();
tick();
fixture.detectChanges();
expect(userService.getUserData).toHaveBeenCalled();
expect(productService.getProductData).toHaveBeenCalled();
expect(component.userData).toBeDefined();
expect(component.productData).toBeDefined();
}));
Этот тест завершается ошибкой с полученным: undefined .Но такой же тест работает при работе с одним наблюдаемым.
Ответ №1:
Я постараюсь ответить на вопрос с помощью другого подхода.
Давайте посмотрим на ваш код, проще тестировать простые фрагменты кода, чем тестировать длинный код.
Будет намного проще назначить переменные вашим свойствам. С помощью этого вы можете просто протестировать свойство
Смотрите ниже улучшенный код;
constructor(
private readonly userService: UserService,
private readonly productService: ProductService
) {}
userData: any;
productData: any;
userData$ = this.userService.getUserData().pipe(
tap(userData => this.userData = userData)
);
productData$ = this.productService.getProductData().pipe(
tap(productData = productData => this.productData = productData)
)
v$ = combineLatest([this.userData$, this.productData$]).pipe(
map(([userData, productData]) => ({ userData, productData }))
)
initialize() {
v$.subscribe()
}
ngOnInit() {
this.initialize();
}
Вернемся к первоначальному тестированию
Теперь давайте попробуем протестировать ваш исходный код и понять, почему тест не работает. Из официальной документации
Когда какой-либо наблюдаемый объект выдает значение, выдает последнее переданное значение из каждого
Давайте посмотрим на приведенный ниже код
spyOn(userService, 'getUserData');
spyOn(productService, 'getProductData');
Выдает ли приведенный выше код какое-либо значение? Ответ отрицательный, поэтому код this.userService.getUserData()
and this.productService.getProductData()
не вернет никакого значения, следовательно, значение не будет выдано. combineLatest
следовательно, не будет выдавать никакого значения.
Решение
Вам нужно будет вернуть Observable
значение из spyOn()
функции, что-то вроде spyOn(UserService, ‘getUserData’).и.returnValue(of({}));
Теперь наблюдаемая будет выдана, и тест пройдет
Последнее предложение по улучшению кода.
Вы можете рассмотреть возможность использования async
канала для обработки вашей подписки. С async
помощью pipe ваш код может быть написан следующим образом
constructor(
private readonly userService: UserService,
private readonly productService: ProductService
) {}
userData$ = this.userService.getUserData()
productData$ = this.productService.getProductData()
v$ = combineLatest([this.userData$, this.productData$]).pipe(
map(([userData, productData]) => ({ userData, productData }))
)
В вашем html
<ng-container *ngIf='v$ | async'>
{{ v.userData | json }}
{{ v.productData| json }}
</ng-container>
В вашем тестовом файле вы можете иметь
it('should initialize user and product data', fakeAsync(() => {
spyOn(userService, 'getUserData').and.returnValue(of({}));
spyOn(productService, 'getProductData').and.returnValue(of({}));
tick();
fixture.detectChanges();
component.v$.subscribe({
next: (v) => {
expect(v.userData).toBeDefined();
expect(v.productData).toBeDefined();
}
})
}));
Приведенный выше тест просто проверяет, что если мы подписываемся на v$
, то userData
определяются и productData
Ответ №2:
Вы можете издеваться combineLatest
jest
так
/* Mock rxjs's combineLatest */
const rxjs = jest.requireActual('rxjs');
rxjs.combineLatest = jest.fn(() => of(<ADD_YOUR_MOCK_DATA_HERE>));
Полный тест выглядит примерно так
it('should initialize user and product data', () => {
const userMockData = <ADD_MOCK_DATA>;
const productMockData = <ADD_MOCK_DATA>;
/* Mock rxjs's combineLatest */
const rxjs = jest.requireActual('rxjs');
rxjs.combineLatest = jest.fn(() => of([userMockData, productMockData]));
jest.spyOn(userService, 'getUserData');
jest.spyOn(productService, 'getProductData');
component.initialize();
expect(component.userData).toEqual(userMockData);
expect(component.productData).toEqual(productMockData);
});