Написание тестов
Тестирование логики управления состоянием — одна из сильных сторон 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. Это позволяет тестировать хранилища и события без влияния на глобальное состояние.
Тестирование эффектов
Эффекты можно тестировать, проверяя их успешное выполнение или обработку ошибок.
В случае 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. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.
Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.