In certain situations, you care more about the final state of the redux store than you do about the particular stream of events coming out of an epic. In this lesson we explore a technique for dispatching actions direction into the store, having the epic execute as they would normally in production, and then assert on the updated store’s state.
In this test file, we've been able to assert that given certain inputs to our search beers Epic, that we received the actions we expect. For example, if we get the wrong action name as the second action here, we get an error here.
This is quite good. There's one crucial thing that is missing. It doesn't allow you to test the interactions with the Redux store. Let's have a look at how it can handle that. It's all going to come down to how we configure the Redux store.
If we look here, all of this code in the index file is related to configuring the Redux store. We're going to extract that into a separate file, and we'll call it configure store. We'll export a function with the same name, paste that in, and then we'll paste in the import as well and get rid of the ones we don't need. Now this files exports this function configure store.
We can go ahead and use that in the index file, get rid of all these unused imports. We can just say that store is equal to configure store. Now we should verify that hasn't broken anything in the browser, so back to the app, let's do a search. We can see everything's still working. That's good.
Now we can utilize this in our tests. If we look at the test we had previously, we're faking the dispatch of an action here and then we are manually subscribing to the output and looking at the actions as they come out. If instead we want to test that it updates the Redux store, we could do that in the following way.
We'll keep this action creator. We'll just say that that's a plain action. Then we have our dependencies. We won't get any output like this. We're not going to sit on the actions either. Instead, we're going to create a store that is the result of calling configure store that we just did. We're going to pass in our dependencies. Then we can dispatch the action created above.
Let's bring this down to make this clearer. We have our dependencies, our store, our action that we want to test, and then we dispatch it. Now if we console log store.get state, we want to be able to make an assertion based on everything that could have happened following this action. If we log in now, we're not going to see what we want just yet.
You can see that at the moment we have message as an empty array, beers as an empty array, and loading is false. It's like nothing's happened yet. Let's go and fix this.
First of all, if we look at the configure store, we're not accepting the dependencies yet. We'll take them here, and we'll say that its default argument is just an object, and here where we're passing in Ajax is the dependency. We can also spread it in those test dependencies.
This means if the object we pass in here has an Ajax property, it will override the one that's already there. That's pretty much all we need to be able to swap out the Ajax implementation. Now we're still not getting what we expect here.
Let's go back to the test and figure out why. We have a time-based operation here. I'm just going to call itself for a moment. See what happens. Now we can see we actually can observe the new state of the Redux store having gone through this whole life cycle. We'll come back to this in a moment.
Let's just look at the amount of code that has just been tested from these few lines. We don't need the Epic directly. We don't need this actions observable. We don't need to bring in any constants here. Instead, we're just going to dispatch this action which is exactly how it will be done inside the component.
This is a higher level test. It might not be suitable for everything you do with Epics, but it's testing a huge amount more of your code. It allows you to assert that the store has been updated in a way you expect.
For example, you could say that you expect get state.beers.length to now be on. In just these few lines here, we've tested that the Epic was registered correctly, that all of this code was correct up to here, that this action creator was correct, and we've even tested that the reducer is correct also.
Because if we was to comment this line out, hit save, you'd see an error. Or if you made a typo or if we tried to access something that didn't exist on the action. You can see that this can be extremely powerful, but what about that line that we had to comment out?
The moment we have something that introduces time, it causes a problem for us. Everything within Redux is expected to be synchronous. When we write line by line like this, we're expecting that everything is completed by the time we get down to our expectation when in reality, this is now taking 500 milliseconds. In JavaScript, that just means that it won't be on the same tick of the event loop as this line. This ends up being incorrect.
To solve this, we can make use of RXJS schedulers. We'll first bring one in called Virtual Time Scheduler. This comes in from RXJS scheduler Virtual Time Scheduler. Now having our dependencies passed in like this becomes extremely useful because we can create our own scheduler. Then we can pass it in as a dependency. Then just after we've dispatched the action, we can flush the scheduler which will execute any queued actions it has built up as fast as possible.
To use it, we have to pass in the scheduler as the second parameter or as the last parameter to anything that accepts a scheduler. If we just pass it in like that, we don't have to change our production code because if an operator in RX accepts a scheduler, it's almost always optional.
What will happen is, in our tests, we'll be passing in the Virtual Time Scheduler. In production, this will be undefined, so RX will just run it with the default scheduler that it was going to use for that particular operator. This all means that if I hit save now, we're back to passing tests.
It's worth noting that this complication of having to use schedulers is only really a problem when you have time involved. If you don't have time, you get away with not doing this inside your Epics. It's worthwhile to know that even very complicated RX workflows can still be unit tested.