Угловой модульный тест combineLatest

#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);
});