Написание тестов
Тестирование логики управления состоянием — одна из сильных сторон Effector. Благодаря изолированным контекстам (fork) и контролируемым асинхронным процессам (allSettled), вы можете проверять поведение приложения без необходимости эмулировать весь его цикл работы.
При помощи вызова функции fork
мы создаем scope
, который можно рассматривать как независимый экземпляр нашего приложения Effector
Основы тестирования
Effector предоставляет встроенные инструменты для:
- Изоляции состояния: Каждое тестируемое состояние может быть создано в своём собственном контексте. Это предотвращает побочные эффекты.
- Асинхронного выполнения: Все эффекты и события могут быть выполнены и проверены с помощью allSettled.
Тестирование сторов
Сторы в Effector тестируются очень просто, так как они представляют собой чистую функцию, управляющую состоянием.
import { counterIncremented, $counter } from "./counter.js";
test("counter should increase by 1", async () => { const scope = fork();
expect(scope.getState($counter)).toEqual(0);
await allSettled(counterIncremented, { scope });
expect(scope.getState($counter)).toEqual(1);});
import { createStore, createEvent } from "effector";
const counterIncremented = createEvent();
const $counter = createStore(0);
$counter.on(counterIncremented, (counter) => counter + 1);
Для изолированного тестирования логики состояния используется fork. Это позволяет тестировать сторы и события без влияния на глобальное состояние.
Тестирование событий
Для того, чтобы протестировать было ли вызвано событие и сколько раз, можно воспользоваться методом createWatch
, который создаст подписку на переданный юнит:
import { createEvent, createWatch, fork } from "effector";import { userUpdated } from "../";
test("should handle user update with scope", async () => { const scope = fork(); const fn = jest.fn();
// Создаем watcher в конкретном scope const unwatch = createWatch({ unit: userUpdated, fn, scope, });
// Запускаем событие в scope await allSettled(userUpdated, { scope, });
expect(fn).toHaveBeenCalledTimes(1);});
Мы не использовали watch
свойство событий, потому что при параллельных тестах мы можем вызывать одно и то же событие, что может вызвать конфликты.
Тестирование эффектов
Эффекты можно тестировать, проверяя их успешное выполнение или обработку ошибок.
В случае unit тестирования мы не хотим, чтобы наши эффекты действительно отправляли запрос на сервер, чтобы избежать этого поведения мы можем передать в fork
дополнительный объект параметр, где в свойство handlers
добавить список пар [эффект, замоканный обработчик]
.
import { fork, allSettled } from "effector";import { getUserProjectsFx } from "./effect.js";
test("effect executes correctly", async () => { const scope = fork({ handlers: [ // Список [эффект, моковый обработчик] пар [getUserProjectsFx, () => "user projects data"], ], });
const result = await allSettled(getUserProjectsFx, { scope });
expect(result.status).toBe("done"); expect(result.value).toBe("user projects data");});
import { createEffect } from "effector";
const getUserProjectsFx = async () => { const result = await fetch("/users/projects/2");
return result.json();};
Полноценный пример тестирования
Например, у нас есть типичный счетчик, но с асинхронной проверкой через наш бэкэнд. Предположим, у нас следующие требования:
- Когда пользователь нажимает кнопку, мы проверяем, меньше ли текущий счетчик чем 100, и затем проверяем этот клик через наш API бэкэнда.
- Если валидация успешна, увеличиваем счетчик на 1.
- Если проверка не пройдена, нужно сбросить счетчик до нуля.
import { createEvent, createStore, createEffect, sample } from "effector";
export const buttonClicked = createEvent();
export const validateClickFx = createEffect(async () => { /* вызов внешнего api */});
export const $clicksCount = createStore(0);
sample({ clock: buttonClicked, source: $clicksCount, filter: (count) => count < 100, target: validateClickFx,});
sample({ clock: validateClickFx.done, source: $clicksCount, fn: (count) => count + 1, target: $clicksCount,});
sample({ clock: validateClickFx.fail, fn: () => 0, target: $clicksCount,});
Настройка тестов
Наш основной сценарий следующий:
- Пользователь нажимает на кнопку.
- Валидация заканчивается успешно.
- Счетчик увеличивается на 1.
Давайте протестируем это:
- Создадим новый экземпляр Scope посредством вызова
fork
. - Проверим, что изначально счет равен
0
. - Затем сымитируем событие
buttonClicked
с использованиемallSettled
– этот промис будет разрешен после завершения всех вычислений. - Проверим, что в конце у нас имеется нужное состояние.
import { fork, allSettled } from "effector";
import { $clicksCount, buttonClicked, validateClickFx } from "./model";
test("main case", async () => { const scope = fork(); // 1
expect(scope.getState($clicksCount)).toEqual(0); // 2
await allSettled(buttonClicked, { scope }); // 3
expect(scope.getState($clicksCount)).toEqual(1); // 4});
Однако в этом тесте есть проблема — он использует реальный API бэкенда. Но поскольку это юнит тест, нам следует каким-то образом подменить этот запрос.
Кастомные обработчики эффектов
Для того, чтобы нам избежать реального запроса на сервер, мы можем замокать ответ от сервера предоставив кастомный обработчик через конфигурацию fork
.
test("main case", async () => { const scope = fork({ handlers: [ // Список пар [effect, mock handler] [validateClickFx, () => true], ], });
expect(scope.getState($clicksCount)).toEqual(0);
await allSettled(buttonClicked, { scope });
expect(scope.getState($clicksCount)).toEqual(1);});
Кастомные значения стора
У нас есть еще один сценарий:
- Счетчик уже больше 100.
- Пользователь нажимает кнопку.
- Должен отсутствовать вызов эффекта.
Для этого случая нам потребуется как-то подменить начальное состояние «больше 100» каким-то образом.
Мы также можем предоставить кастомное начальное значение через конфигурацию fork
.
test("bad case", async () => { const MOCK_VALUE = 101; const mockFunction = testRunner.fn();
const scope = fork({ values: [ // Список пар [store, mockValue] [$clicksCount, MOCK_VALUE], ], handlers: [ // Список пар [effect, mock handler] [ validateClickFx, () => { mockFunction();
return false; }, ], ], });
expect(scope.getState($clicksCount)).toEqual(MOCK_VALUE);
await allSettled(buttonClicked, { scope });
expect(scope.getState($clicksCount)).toEqual(MOCK_VALUE); expect(mockFunction).toHaveBeenCalledTimes(0);});
Вот так мы можем протестировать каждый случай использования, который хотим проверить.
Документация на английском языке - самая актуальная, поскольку её пишет и обновляет команда effector. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.
Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.