Мраморное тестирование - отличный способ проверить наблюдаемые. Он фокусируется на поведении наблюдаемых во времени. Комбинация тестового утверждения и мраморной диаграммы позволяет визуализировать изменение выдаваемых значений во времени.
Написание тестов 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);
Мы готовы собрать тест вместе:
А теперь посмотрим на результат теста!
Давай попробуем, если тоже сработает.
Не стесняйтесь взглянуть на собранную мной демонстрацию. Вы можете найти более подробную информацию о том, как набирать эпики и наблюдаемые там.
Вот оно! Спасибо, что прочитали.
Надеюсь, я сделал это настолько простым, насколько это было возможно. Если у вас есть мысли или что-то вам непонятно, не стесняйтесь оставлять комментарии ниже или связываться со мной в твиттере!
Если вы поклонник функционального программирования, ознакомьтесь с этой статьей, которую я написал о преобразователях. Это пошаговое объяснение написания преобразователя, затрагивающее ключевые идеи функционального программирования.
Удачного кодирования!