Мраморное тестирование - отличный способ проверить наблюдаемые. Он фокусируется на поведении наблюдаемых во времени. Комбинация тестового утверждения и мраморной диаграммы позволяет визуализировать изменение выдаваемых значений во времени.

Написание тестов Marble с TypeScript для redux-observable иногда может быть немного сложным. Найти инструкции или передовой опыт не так-то просто. Мне потребовалось некоторое время, чтобы освоиться с этим, поэтому я хочу поделиться с вами своими выводами, и, надеюсь, это даст вам лучшее представление о том, как тестировать свои эпики.

Я собрал быструю демонстрацию. Пожалуйста, обратитесь к нему, чтобы узнать о типах, действиях и редукторе.

TL;DR

  • Используйте метод createTime() из TestScheduler, чтобы имитировать ответ об ошибке API.
  • Тестирование эпиков с action$ как горячо наблюдаемое.
  • state$ в эпосе - это StateObservable, который принимает Тема и состояние вашего магазина Redux.
  • Собирайте шариковые диаграммы вместе, чтобы помочь людям лучше понять ваши тесты.
  • Для демонстрации, пожалуйста, загляните здесь.

Давайте вместе рассмотрим сценарий:

Пользователю предоставляется список пользователей Github в веб-приложении. Щелкнув элемент списка, мы получим общедоступные репозитории соответствующего пользователя Github и отобразим их.

При выборе пользователя Github мы отправляем UPDATE_SELECTED_USER действие для обновления selectedUser в магазине. После обновления мы хотим отправить fetchRepos действие, чтобы сигнализировать об извлечении из Github. После получения ответа API магазин будет обновлен с сообщением об успешном ответе или ошибке. Последовательность действий выглядит так:

Тестирование наблюдаемых, выполняющих выборку

Мы хотим начать с написания сервиса для запроса списка репозиториев. Мы сохраняем данные ответа или обновляем статус ошибки в зависимости от успешности запроса.

Для нашего первого теста мы хотим увидеть, вызывает ли успешный запрос сразу fetchReposSuccessful действие в службе. Диаграмма мрамора и стоимость мрамора выглядят так:

const marbles = {
  i: '-i', // api response
  o: '-o', // output action
};
const values = {
  i: response,
  o: fetchReposSuccessful(response),
};

Обратите внимание, что мы вместе перечисляем мраморные диаграммы. Я считаю, что это очень четкое представление, показывающее излучаемые значения с течением времени. Мы можем добавить описания или промежуточные шаги в эту структуру, чтобы улучшить читаемость:

const marbles = {
  i: '-i', // api response
  // == tap ==  
  // '-i'
  // == map ==
  o: '-o', // output action
};

Далее, как и в официальной документации, мы заключим наш тест в TestScheduler.run.

scheduler.run(({ cold, expectObservable }) => {      
  const getJSON = (url: string) => cold(marbles.i, values);        
  const output$ = fetchGithubRepos('test-user', getJSON);      
  expectObservable(output$).toBe(marbles.o, values);    
});

Готово ✓ Теперь мы проверяем, излучает ли наблюдаемый объект fetchReposFailed при сбое запроса API. Сложная часть теста - имитировать неудавшийся запрос. Что мы можем сделать, так это создать наблюдаемое с помощью timer.

Замечательно то, что мы можем создать временной интервал для timer с помощью TestScheduler.

const duration = scheduler.createTime('-|');      
const getJSON = (url: string) => 
  timer(duration).pipe(mergeMap(() => throwError(error)));

Вместо того, чтобы указывать время продолжительности в миллисекундах, мы создаем timer с фиктивной продолжительностью в мраморной нотации и TestSchedule. Поэтому, когда мы выполняем getJSON, он выдаст ошибку после одного тика в планировщике. Ожидаемый результат выглядит так:

const marbles = {      
  d: '-|', // mock api response time duration
  o: '-(o|)', // output action. Complete when error thrown.
};    
const values = {      
  o: fetchReposFailed(error),    
};
scheduler.run(({ expectObservable }) => {             
  const output$ = fetchGithubRepos('test-user', getJSON as any);        
  expectObservable(output$).toBe(marbles.o, values);    
});

Вы можете найти полный тест ниже.

Тестирование эпиков без состояния $

Мы готовы написать первую эпопею, которая использует службу fetchGithubRepos для получения репозиториев.

Это прямая эпопея. Мы хотим прослушать тип действия FETCH_REPOS_REQUESTED, выбрать имя пользователя из полезной нагрузки действия и выполнить наблюдаемую службу с именем пользователя. Обратите внимание, что мы используем switchMap, поэтому мы можем отменить дублированный запрос API.

Чтобы протестировать эпос, мы хотим увидеть, слушает ли он правильное действие и генерирует ли оно только самое последнее наблюдаемое действие. Шарики выглядят так:

const marbles = {      
  r: '--r', // mock api response      
  i: '-ii--i', // input action
  // == switchMap() ==      
  // only emit the latest value for consecutive inputs
  // '----r--r'
  // == map() ==
  o: '----o--o', // output action    
};
const values = {      
  i: fetchRepos('test-user'),      
  r: response,      
  o: fetchReposSuccessful(response),    
};

Теперь построим тест.

scheduler.run(({ hot, cold, expectObservable }) => {      
  const action$ = hot(marbles.i, values) as any;      
  const state$ = null as any;      
  const dependencies = {        
    getJSON: (url: string) => cold(marbles.r, values),      
  };      
  const output$ = 
    fetchGithubReposEpic(action$, state$, dependencies);        
  expectObservable(output$).toBe(marbles.o, values);    
});

Чтобы имитировать наблюдаемые входные действия, мы используем помощник hot из функции обратного вызова scheduler.run для создания горячих наблюдаемых. Наблюдаемые действия имеют холодный характер; Однако, когда они проходят через промежуточное программное обеспечение redux-observable, наблюдаемые действия становятся горячими, если субъекты устанавливаются в качестве наблюдателей. Подробнее о холодных и горячих наблюдаемых читайте в статье Бена Леша. В статье он также коснулся того, как сделать код наблюдаемым.

Вы можете видеть, что мы утверждаем action$ как any для fetchGithubReposEpic использования. Это потому, что Epic ожидает, что action$ будет типа ActionsObservable вместо типа ColdObservable. CodeObservable extends Observable и является наблюдаемым помощником в TestScheduler для облегчения тестирования. Можно смело утверждать.

Мы присвоили state$ null, потому что у нас нет доступа к магазину в эпопее.

Что касается зависимостей, мы имитируем getJSON с помощью cold помощника, чтобы имитировать успешный запрос API.

Тестирование Epic с состоянием $

Теперь мы можем завершить последнюю часть, чтобы завершить весь поток действий.

Мы создаем эпос, который слушает updateSelectedUser действие и использует только что обновленное имя пользователя для запуска fetchGithubRepos наблюдаемого сервиса.

Чтобы проверить это, нам нужно знать, как имитировать наблюдаемые состояния. Наблюдаемые состояния (state$) фактически создаются StateObservable. StateObservable принимает в качестве параметров субъект и состояние Redux.

const reduxState: AppState = {
  selectedUser: 'test-user',        
  githubRepos: reposInitialState,
};
const state$ = new StateObservable<AppState>(
  new Subject(), reduxState);

Мы готовы собрать тест вместе:

А теперь посмотрим на результат теста!

Давай попробуем, если тоже сработает.

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

Вот оно! Спасибо, что прочитали.

Надеюсь, я сделал это настолько простым, насколько это было возможно. Если у вас есть мысли или что-то вам непонятно, не стесняйтесь оставлять комментарии ниже или связываться со мной в твиттере!

Если вы поклонник функционального программирования, ознакомьтесь с этой статьей, которую я написал о преобразователях. Это пошаговое объяснение написания преобразователя, затрагивающее ключевые идеи функционального программирования.

Удачного кодирования!