Декларативные эффекты

В redux-saga саги реализованы с использованием функций генераторов. Чтобы выразить логику сагов, мы используя ключевоео слово yield и передаем простые объекты JavaScript из генератора. Мы называем их объекты Effects. Effect это простой объект, который содержит информацию, которая должна интерпретироваться с помощью middleware. Вы можете просматривать эффекты, как инструкции в middleware которые выполняют какую-то операцию (вызовите любую асинхронную функцию и диспачните её в ваш Store).

Чтобы создать effects, вы можете используете функции которые находятся в пакете redux-saga/effects.

В этом разделе и ниже мы раскажем об основных Effects-ах. И рассмотрим, как эта концепция позволяет легко тестировать вашии саги.

Саги могут отдавать Effect в нескольких формах. Самый простой способ используя ключивое слово yield вернуть Promise.

Например, предположим, что у нас есть сага, которая следит за action PRODUCT_REQUESTED. При каждом диспатче action PRODUCT_REQUESTED, запускается задача которая получает список продуктов с сервера.

import { takeEvery } from 'redux-saga/effects'
import Api from './path/to/api'

function* fetchProducts() {
  const products = yield Api.fetch('/products')
  console.log(products)
}

function* watchFetchProducts() {
  yield takeEvery('PRODUCTS_REQUESTED', fetchProducts)
}

В приведенном выше примере мы вызываем Api.fetch непосредственно изнутри генератора (в функциях генераторах, любое выражение справа от yield вычисляется, и только потом отдается).

Api.fetch('/products')запускает AJAX-запрос и возвращает Promise that will resolve with the resolved response, the AJAX request will be executed immediately.

Просто, но...

Предположим, мы хотим создать тест для генератор выше:

const iterator = fetchProducts()
assert.deepEqual(iterator.next().value, ??) // что мы ожидаем?

Мы хотим проверить результат первого значения, полученного из генератора. В нашем случае это результат выполнения Api.fetch('/products'), который является Promise-ом.

Выполнение реального сервиса во время тестов не является ни жизнеспособным, ни практическим подходом, поэтому приходится мокать результат функции Api.fetch , т. е. мы должны заменить реальные функции фейковыми, которая на самом деле не делают AJAX-запрос, а только проверяет, что Api.fetch с правильными аргументами ('/products' в нашем случае).

Моки делают тестирование более сложным и менее надежным. С другой стороны, функции которая просто возвращают значения легче проверить, так как мы можем простую использовать equal(), чтобы проверить результат.

Это способ позволяет написать самые надежные тесты.

Не верите? Советую вам прочитать Eric Elliott's article:

(...)equal(), by nature answers the two most important questions every unit test must answer, but most don’t:

  • What is the actual output?
  • What is the expected output?

Если вы закончите тест, не отвечая на эти два вопроса, у вас нет реального модульного теста. У вас будут только неаккуратные тесты.

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

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

// Effect -> вызывает функцию Api.fetch с аргументом `./products` 
{
  CALL: {
    fn: Api.fetch,
    args: ['./products']
  }
}

Иными словами, генератор будет отдавать простые объекты, содержащие инструкции и middleware redux-saga будет заботиться о выполнении этих поручений и результат их выполнения для генератора. Таким образом, при тестировании Генератора, все, что нам нужно сделать, это проверить, что он отдает ожидаемую инструкцию, просто вызывая deepEqual на данном объекте.

По этой причине библиотека предоставляет другой способ выполнения асинхронных вызовов.

import { call } from 'redux-saga/effects'

function* fetchProducts() {
  const products = yield call(Api.fetch, '/products')
  // ...
}

import { call } from 'redux-saga/effects'
import Api from '...'

const iterator = fetchProducts()

// expects a call instruction
assert.deepEqual(
  iterator.next().value,
  call(Api.fetch, '/products'),
  "fetchProducts should yield an Effect call(Api.fetch, './products')"
)

Теперь мы используем функцию call(fn, ...args). Отличие от предыдущего примера заключается в том, что теперь мы не будем получать данные используя fetch сразу, вместо этого мы вызовем call для создания описания Effect-а.

Так же, как в Redux вы используете action creator для создания простого объекта, описывающего действие, которое будет выполняться Store, call создает простой объект, описывающий вызов функции. Middleware в Redux-saga заботится о выполнении вызова функции и возобновлении работы генератора с полученым ответом.

Это позволяет легко проверить генератор вне среды Redux-а. Потому что call это просто функция, которая возвращает простой объект.

import { call } from 'redux-saga/effects'
import Api from '...'

const iterator = fetchProducts()

// ожидается вызов
assert.deepEqual(
  iterator.next().value,
  call(Api.fetch, '/products'),
  "fetchProducts should yield an Effect call(Api.fetch, './products')"
)

Теперь нам не нужно мокать какие-либо данные и нам достаточно простой проверки полученных данных на равенство.

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

call также поддерживает вызов объектных методов, вы можете представить this в контексте вызываемой функции, используя следующую форму:

yield call([obj, obj.method], arg1, arg2, ...) // as if we did obj.method(arg1, arg2 ...)

applyявляется алиасом для формы вызова метода

yield apply(obj, obj.method, [arg1, arg2, ...])

call и apply хорошо подходят для функций, которые возвращают результаты в виде Promise.

Другая функция cps может использоваться для обработки функций в стиле Node (например,fn(...args, callback), где callback имеет вид (error, result) => ()).

Пример:

import { cps } from 'redux-saga/effects'

const content = yield cps(readFile, '/path/to/file')

И, конечно, вы можете написать тест для него также же, как бы вы написали для call:

import { cps } from 'redux-saga/effects'

const iterator = fetchSaga()
assert.deepEqual(iterator.next().value, cps(readFile, '/path/to/file') )

cps также поддерживает ту же форму вызова метода, что иcall.

results matching ""

    No results matching ""