Как выполнить модульное тестирование отмены запросов RXJS в полете

Я использую NGRX и использую эффекты для отправки HTTP-запросов. Если пользователь отправляет другой запрос, любой предыдущий запрос должен быть отменен. Это работает нормально, когда я тестирую вручную, но я хочу иметь возможность тестировать этот модуль. Чтобы проверить это, я издеваюсь над сервисом, который отправляет HTTP-запрос и отправляет ответ после определенной задержки. Затем у меня есть горячая наблюдаемая Мраморная, которая вызывает 4 запроса. Я ожидаю, что мой эффект срабатывает только один раз. Тем не менее, это не срабатывает вообще.

Модульный тест:

it('should only do one request at a time', fakeAsync(() => {
    // Arrange
    const data = createTestData();
    const dataServiceSpy = TestBed.get(DataService);
    dataServiceSpy.getData = jest.fn(
        (query: DataQuery) => {
            const waitTime = 1000 * + query.index;
            return of(assets).pipe(delay(waitTime));
        }
    );         
    // Act
    actions = hot('-abcd-|', {
        a: new SearchData({ index: '6' }),
        b: new SearchData({ index: '5' }),
        c: new SearchData({ index: '4' }),
        d: new SearchData({ index: '1' })
    });

    tick(10000);

    // Assert
    expect(effects.loadData$).toBeObservable(
        hot('-a-|', { a: new SearchDataComplete(assets) })
    );
}));

Итак, я отправляю 4 поисковых запроса; первый должен возвращать данные через 6 секунд, второй после 5 и так далее. Тем не менее, мой модульный тест терпит неудачу, что loadData$ является пустой наблюдаемой, в то время как он ожидает иметь один элемент.

Если я уберу задержку в шпионе, она будет работать как положено, а loadData$ содержит 4 результата.

My Effect использует NX DataPersistence, которое заботится об отмене, если вы предоставляете функцию id; первоначальный запрос будет отменен, если поступит новое действие с тем же идентификатором. Это похоже на использование this.actions$.pipe(switchMap(...))

@Effect()
loadData$ = this.dataPersistence.fetch(ActionTypes.SearchData, {
    id:  (action, state) => {
        return action.type
    },

    run: (action, state) => {
        return this.dataService
            .searchData(action.payload)
            .pipe(                   
                map(data => new SearchDataComplete(data))
            );
    },

1 ответ

Так что я немного погрузился в это. У меня есть две мысли:

  1. В модульном тесте мы действительно просто хотим протестировать код, который мы пишем. Если я использую стороннюю библиотеку, я бы сделал предположение, что она должным образом проверена модулем (скажем, посмотрев исходный код этой библиотеки). Модульные тесты для DataPersistence не тестируют на отмену прямо сейчас (потому что мы используем switchMap и делаем предположение, что его функциональность работает).
  2. Существует актуальная проблема при попытке проверить с delay в примере

В ваших тестах tick срабатывает до подписки Эффекта (когда вы вызываете expect ниже этого).

Один из способов обойти это, как показано ниже:

describe('loadTest$', () => {
    it('should only do one request at a time', () => {

      // create a synchronous scheduler (VirtualTime)
      const scheduler = new VirtualTimeScheduler();

      // Arrange
      const data = createTestData();
      const dataServiceSpy = TestBed.get(DataService);

      TestBed.configureTestingModule({
        imports: [NxModule.forRoot(), StoreModule.forRoot({})],
        providers: [
          TestEffects,
          provideMockActions(() => actions),
          {
            provide: DataService,
            useValue: {
              searchData: jest.fn(
                  (query: DataQuery) => {
                      const waitTime = 1000 * + query.index;
                      return of(assets).pipe(delay(waitTime, scheduler));
                  }
              )
            }
          }
        ]
      });

      actions = of(
          new SearchData({ index: '6' }),
          new SearchData({ index: '5' }),
          new SearchData({ index: '4' }),
          new SearchData({ index: '1' })
      );

      const res = [];
      effects.loadData$.subscribe(val => res.push(val));

      scheduler.flush(); // we flush the observable here

      expect(res).toEqual([{ a: new SearchDataComplete(assets) }]);
    });
});

Мы можем использовать синхронный планировщик и вручную очистить его. Обычно нам не нужно ничего делать; это просто из-за delay что нам нужно (это также произошло бы с другими операторами, которые нуждаются в планировщике, как debounceTime).

Надеюсь это поможет. Я думаю, что ваши тесты не должны были бы проверять функциональность базовой библиотеки (в этот момент это может быть не строгий модульный тест, а скорее интеграционный тест).

Другие вопросы по тегам