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

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

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