In this lesson we will utilize generator functions and effect objects to test our sagas. We’ll walk through different scenarios and how to test each case with Jest.
Instructor: [00:00] Let's test our "fetchPerson" saga. We go to our actions test file and import "call" and "put" as well as our types, and our "fetchPerson" saga as well as the API function.
[00:12] Now we can write our first test. We'll do "describe fetchPerson" and "const personGen = fetchPerson." We'll do "it('should hit API', ()." We'll do "expect(personGen.next().value).toEqual(call)" with API function in the URL.
[00:33] If we look back to our "fetchPerson" saga, we see that we're doing two things -- first, calling the Star Wars API, and, second, dispatching an action creator with the response data. The fact that this is a generator function, and we are using saga effects, we can just test that the right effect is called with the right parameter.
[00:53] Saga effects are also referred to as effect objects. The effect consumes what it is given and creates an object with a set of instructions to send to the saga middleware to execute. Inside of our test, we bring in all the used saga effects to test this logic.
[01:11] First, we invoke and assign our "fetchPerson" to a const. Then we iterate through the generator object to replicate each effect. Here we're testing to make sure that the first iteration we're using the "call" effect with these parameters. Then the second iteration will be the "put."
[01:30] We'll do "it('on success dispatch success action', ()," "const person" equals an object, results an array "expect(personGen.next(person).value).toEqual(put)" with our type and our data. Now, on the second iteration of personGen, we're using the "put" effect.
[01:57] We're creating some mock data, and assigning it to "person," and passing it through on the iteration. We need this mock data because saga middleware is not handling the iteration for us. The middleware has additional logic to handle the saving of values.
[02:11] As we iterate and the generator object looks back on this yield from "person," it would not know what the value is. We're saying that yield equals this mock data. Then we test the "put" with this information. We can check return row and see that our two tests are passing. "Call" and "put" are being called on the correct iteration of the generator function with the correct parameters.
[02:36] Now that we have some tests for our simple saga, let's create a more complicated one and test it. Inside of our actions file, we'll create that new saga called "forked(fetchPerson)." We'll do "const syncPersons = yield fork(fetchPerson)," "yield take" and a type, and then "yield cancel" seeing "persons." Then we'll import "fork," "take," and "cancel" up here at the top.
[03:00] Inside a "forked(fetchPerson)," we are using the non-blocking call "fork" and then adding a way for the user to cancel this API request. If the store receives an action with type, we want to cancel the forked requests.
[03:14] Now, for our test, we need to import "forked(fetchPerson)." Then we'll do a new describe. We'll do "describe('forked(fetchPerson)', ()." We'll do "const forkGen = forked(fetchPerson)." We'll do "it('forks the service', ()" with "const expectedYield = fork(fetchPerson)" and then "expect(forkGen.next().value).toEqual(expectedYield)."
[03:41] With this second describe of "forked(fetchPerson)," we're not iterating on the "forked(fetchPerson)" generator. With this first "it," we want to make sure that the "fork" effect is called with the right "fetchPerson" generator. If we look back at our saga, we can see that after we do our "fork" we do a "take."
[03:59] Let's go and make sure that our "take" is working correctly. We'll make a new "it." We'll say "it('waits for stop action and then cancels the service', ()." We'll do "const expectedTakeYield = take" and a type. Then we'll do "expect(forkGen.next().value).toEqual(expectedTakeYield)."
[04:22] With this iteration, we're testing to make sure that "take" is called with the right type. Now we need to bring in "fork," "take," and "cancel" as well as a util called "createMockTask." Now we can finish off this "it."
[04:37] We'll do "const mockTask = createMockTask()." We'll pass it through on "take" as well as do "const expectedCancelYield = cancel(mockTask)," then "expect(forkedGen.next().value).toEqual(expectedCancelYield)."
[04:55] Because "fork" is a non-blocking call, it can resolve at any point and is unknown to the generator when it does resolve. This async nature requires us to mock out the saga or running task. We need to use this "createMockTask" util.
[05:10] Because a saga middleware carries along any running task with each iteration, we need to mimic this behavior. We do so by passing this mock task through on the "take" iteration. Finally, we test to make sure the last iteration yields the cancel effect in the mock task. If we save and pull up our terminal, we can see that all of our tests are passing.
^^^^
What Kevin said
That is a great point Kevin... I have seen others create a wrapper function that takes the saga generator and the second parameter the number of "next()" that's needed to get to the step you are actually testing. So it's a new generator instance for each test. That way you can move the tests around and it still works. Still not very pretty but solves that issue.
BTW it does give you some sort of testing ability compared to not being able to test at all if you used Redux Thunk.
Does anyone have an alternative to createMockTask? redux-saga/utils is no longer a part of redux-saga.
I like Saga's but I hate testing them. It feels they are testing implementation. Aside from that, if you'd comment out your first "it", the second "it" would fail, because the second assume the first one ran already, no? If so that's not great imo. Re-arranging the tests would break them = bad.