Here is a non-trivial Epic. it powers the search feature by listening to actions of this type, it debounces the user's input, it filters, it sets the UI into a loading state, and then it makes an Ajax request that can be canceled mid-flight. It handles errors and is typical of the thing you'll see in a Redux observable app.
Let's see how we can unit test this. I've created a test file alongside this one, and I've appended .test to the end. This means that the test runner set up by create React app will automatically pick up this file.
Now looking at this, it seems like the first thing we need to do is mock this actions observable and have it produce an action with the type searched beers. To do that, we can use this actions observable which we're imported from Redux observable. We can say that actions is equal to action observable of.
Then here we want to create the action as we would inside the application. If we look back here and if we command click on search beers and find the action creator responsible for this, we can see here it's this search beers function. This just takes a query string. If we copy that back to our test, we can call it. We'll just import that function. Let's just call it with Shane.
The actual value here doesn't really matter because we're not going to be sending it over the network. All that really matters is that the type of the argument is correct, in this case, a string. That's our input.
Now we can say that the output is going to be equal to the results of calling the Epic which we export here and passing along the actions observable we've just created above. Because search beers Epic returns an observable, it means we can subscribe to and have a look what comes out the other side. If we just say output.subscribe, this will receive each action. Then we'll just log it.
Let's see what happens. OK, not bad. We have a first console log showing search beers loading. Let's have a look at that. That means we must have made it through these two. We've created a loading observable here, and it gets passed into this.
We've got here, but then we get an error. That must mean that we've entered this. Why is that? It's because this test runner in running in a Node environment, and we're calling observable.ajax which expects the XHR object to be available on the window. This is actually a good sign because it shows that this function alone is not testable without the correct environment. From all this functionality, it's literally just this line that's causing this test to fail.
This brings us on to the idea of passing in dependencies to these Epics. What if instead we were to pass in just this part as a parameter here? That would allow us in our tests to mock what comes back from this function call. When we're in the production environment, we will just pass in observable.ajax as normal. That sounds good.
Let's see how to implement that. First of all, before we call the function, we need to create our dependencies. In this case, we're going to pass in the entire Ajax object. We know that in this particular Epic, we only actually call get JSON. We can just mock that. We're going to say that's a function that returns observable of. Here is where we can just fake what comes back from the network.
For example, we can just say that it returns an array of objects, and one of the objects has the name Shane. Now we need to pass in this as the third argument to the search beers Epic. We'll say null here. This is where the store will be in a production environment which we'll see in a moment. We'll pass in dependencies as the last argument.
Now inside this function, we just need to swap out observable.ajax for our dependencies.ajax. It means that we can fake what comes back from the network. This is actually a great technique if you have example and data, like if you have a folder full of JSON files that describe an API, you can bring those into these tests and return them here.
If we hit save, you'll see we still have the error. That's because we need to go into our Epic now, and we'll get the store as the second argument. We'll get those dependencies as the third. Then we'll just swap out observable.ajax for dependencies.ajax. Now hit save. You can see straightaway we get the received beers action.
Now we can start building up some tests around this. We'll call to array on this to aggregate all of the actions into a single array. We'll rename it to actions. Then we can start adding our expectations. We can say that actions.length should be two. We can get more granular, and we can say things like first action, type property of that should be search beers loading. The second one should have been received beers.
With those in place, you can go back to your Epic and just comment out a couple of lines to make sure the test can fail. As you can see, that one does. Put it back in and it's working. Now you can do any kind of assertions you want on the actions that come out of your Epic.
At this point, if we go back to the browser, you can see that we've actually broken the application which is a bit odd to say that we're adding tests. It's all about the fact that we've introduced this dependency now.
If you go back to the place in our application where we register the Epics, that's in this index file, create Epic middleware, that we import from Redux observable accepts a second parameter. If we provide the key dependencies, this allows us to pass in anything that we want, and it'll be provided as that third argument.
If we look back at our tests, we can see that we're passing in an object that has the key of Ajax and has a get JSON method. We did that to replace the observable implementation. If we go to that index file, we'll make this an object, and we'll say that Ajax is equal to observable.ajax. If we just import that like this, then we can make this whole thing a bit shorter because we can just get rid of that.
Now we're saying that for every Epic registered on this root Epic, as its third argument, pass in this object here. Then you can see in our Epic there, that's the third argument, and it has Ajax property. We can call get JSON on it. One more time we'll test the app. You can see it's working again as we expected.
One last note is that we're not restricted to just passing in things to do with Ajax. For example, in a React native application, you may pass in things that do alerts or open up a URL in the maps application or anything else that talks to the outside world. The moment you need to test anything like that you can just pass it in as a dependency and the logic around how you actually do it can be tested without a problem.