To create Effects, you use the functions provided by the library in the
In this section and the following, we will introduce some basic Effects. And see how the concept allows the Sagas to be easily tested.
Sagas can yield Effects in multiple forms. The easiest way is to yield a Promise.
For example suppose we have a Saga that watches a
PRODUCTS_REQUESTED action. On each matching action, it starts a task to fetch a list of products from a server.
In the example above, we are invoking
Api.fetch directly from inside the Generator (In Generator functions, any expression at the right of
yield is evaluated then the result is yielded to the caller).
Api.fetch('/products') triggers an AJAX request and returns a Promise that will resolve with the resolved response, the AJAX request will be executed immediately. Simple and idiomatic, but...
Suppose we want to test the generator above:
We want to check the result of the first value yielded by the generator. In our case it's the result of running
Api.fetch('/products') which is a Promise . Executing the real service during tests is neither a viable nor practical approach, so we have to mock the
Api.fetch function, i.e. we'll have to replace the real function with a fake one which doesn't actually run the AJAX request but only checks that we've called
Api.fetch with the right arguments (
'/products' in our case).
Mocks make testing more difficult and less reliable. On the other hand, functions that return values are easier to test, since we can use a simple
equal() to check the result. This is the way to write the most reliable tests.
Not convinced? I encourage you to read 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?
If you finish a test without answering those two questions, you don’t have a real unit test. You have a sloppy, half-baked test.
What we actually need to do is make sure the
fetchProducts task yields a call with the right function and the right arguments.
Instead of invoking the asynchronous function directly from inside the Generator, we can yield only a description of the function invocation. i.e. We'll yield an object which looks like
Put another way, the Generator will yield plain Objects containing instructions, and the
redux-saga middleware will take care of executing those instructions and giving back the result of their execution to the Generator. This way, when testing the Generator, all we need to do is to check that it yields the expected instruction by doing a simple
deepEqual on the yielded Object.
For this reason, the library provides a different way to perform asynchronous calls.
We're using now the
call(fn, ...args) function. The difference from the preceding example is that now we're not executing the fetch call immediately, instead,
call creates a description of the effect. Just as in Redux you use action creators to create a plain object describing the action that will get executed by the Store,
call creates a plain object describing the function call. The redux-saga middleware takes care of executing the function call and resuming the generator with the resolved response.
This allows us to easily test the Generator outside the Redux environment. Because
call is just a function which returns a plain Object.
Now we don't need to mock anything, and a basic equality test will suffice.
The advantage of those declarative calls is that we can test all the logic inside a Saga by iterating over the Generator and doing a
deepEqual test on the values yielded successively. This is a real benefit, as your complex asynchronous operations are no longer black boxes, and you can test in detail their operational logic no matter how complex it is.
call also supports invoking object methods, you can provide a
this context to the invoked functions using the following form:
apply is an alias for the method invocation form
apply are well suited for functions that return Promise results. Another function
cps can be used to handle Node style functions (e.g.
fn(...args, callback) where
callback is of the form
(error, result) => ()).
cps stands for Continuation Passing Style.
And of course you can test it just like you test
cps also supports the same method invocation form as
A full list of declarative effects can be found in the API reference.