Test Epics with Marble Diagrams

Shane Osbourne
InstructorShane Osbourne

Share this video with your friends

Send Tweet
Published 4 years ago
Updated 3 years ago

Epics can be unit-tested just like any other function in your application - they have a very specific set of inputs (the action$ stream) and the output is always an Observable. We can subscribe to this output Observable to assert that the actions going back into the Redux are the ones we expect.

Instructor: [00:00] Considering how simple this application looks, it's actually rather complex. On every keystroke we're emitting actions, we are filtering them, debouncing them, another filter. We are combining with the streams, making AJAX requests, handling success, handling failure.

[00:25] Also allowing all of the above to be canceled via click or canceled via an escape key. If any of those happen, we map those into a different action. Then we're setting the status and we're allowing those two streams there to race against each other.

[00:44] This is a non-trivial piece of code. Watch how easily I can break this. If I just spell this incorrectly, and granted, this could be solved by using a constant or a type system, but I'm just trying to show how an error like that can only be picked up in the application when the loading spinner didn't show there.

[01:09] If this had taken five seconds to come back and there was no loading spinner, the user may have thought this application was broken. Likewise, we could return data in the wrong format. For example, this fetch fulfilled which creates an action that is responsible for saving the data into the store, if we were to do something simple like wrap that in an array, again we're going to get a complete failure.

[01:41] Even though we're doing all these asynchronous things and we've got time involved, the fact that we're using Rx means that even this is still relatively easy to test. If we think about what an epic does, at its most fundamental level it's just a function that accepts three parameters, and it returns a stream that produces actions.

[02:04] Providing we have a way to actually call this function with these three items, then we could assert on the values that get produced. Inside the epics folder, we'll create a file on the tests. We'll say fetchbeers.test.js. Now we can give ourselves some room here and bring in our terminal. Since Create React App comes with Jest already set up for us, all we have to do is type NPM test.

[02:42] It's going to go into watch mode and it's basically saying that we don't have any tests yet, which is fine. Our first test can just be that it produces the correct actions. We'll be using the test scheduler from RxJS, so we can import that. That comes from RxJS testing.

[03:07] We create a new instance of it and it gives us a callback with an actual and expected parameters. This is done so that you can adapt it to suit your testing framework. For example, in Jest we have this global expect, what we're doing here is we're teaching the test scheduler how to compare two objects.

[03:26] Next, we call the run method on the test scheduler. This gives us a callback with access to these helpers for hot observables, cold observables, and for this expect observable. We'll see what those are used for in a moment.

[03:40] If we look back out at epic, what we said is that we just have these three items. We need to create these. We'll have an action stream, a state stream, and some dependencies. The action stream will be a hot observable, just produces a single element.

[04:07] That element will be the search action so we can reuse the action creator. We'll give it a value of ship as though we just typed that into the user interface. This is using the marble diagrams from RxJS. We could have had some frames before this, but I'm just going to say that it produces an element immediately.

[04:29] Next, for the state, inside our epic we only actually use the config from the state. That comes from here. Why don't we export this so that we can reuse this initial state in our test? State will be an observable. We'll need to put the config key just like we do when we configure the store. This can be the initial state. That comes in from that config reducer that we just exported there.

[05:12] Now, the part that we want to stop here is this getJSON, so that we don't actually have to make a network request. We can say that the dependencies are getJSON. This is a function that takes a URL, but we don't care about that.

[05:27] We're just going to return a cold observable that after one frame produces a value. The value it produces will just be an array, and we're just going to give it a name of beer one. We know this API always returns beers under an array. We don't care what the data is at the moment, just that it's roughly the right shape. Those are the three arguments for our epic.

[05:54] Now we can create an output stream which is the result of calling the fetch beers epic. We'll import that and we'll pass along actions, state, and dependencies. Now we can assert on the output. If we just nip back to the browser, if we type ship here just like in our test, go to the Redux store, you'll see that we get these four search events which we're using the debounce to guard against.

[06:31] Then we get a set status and a fetch fulfilled. In our test what we want to assert is that when a search happens, it matches this, that we see the following two actions be produced by the epic. To assert that we use the expect observable helper that we're given here. We pass in our output. This will subscribe to the output and allow us to assert on what it produces. We can do that in the marble format again.

[07:03] As a first attempt, let's just say we produce two values, A and B. Those two values should be a set status action. Again, we're reusing the action creators here. We should set status depending, then we should follow up with a fetch fulfilled, which should match the getJSON stub that we have here. We'll just copy paste that in there. You see we get an error.

[07:40] We expected to see a value at frame zero, which is what this marble diagram denotes here. Actually, what we received was an event of frame 500. Why was that? If you look here, because we're using debounce time, it means that we'll see a window of 500 milliseconds before any events.

[08:04] The marble diagrams actually have a feature where you can save 500 MS and put a space. The space doesn't do anything other than separate the thing on the left with the thing on the right. Then we get a pass. Let's make this fail a little bit so we can get a bit clearer on what's happening here.

[08:25] If we put an extra frame in there and look at the difference, you can see that we expected 502. This is allowing us to skip ahead 500 milliseconds. This is on the 500 frame. This is 501 and this is 502. The reason we get a frame on 501 is because we have a 500 millisecond delay here. Then in our stub we said that the AJAX request will come back after one frame. Now the test passes again.

[09:05] This is an incredibly powerful framework for testing because with just one single test here, we've been able to execute all or the bulk of the logic concerning what is quite a complicated feature. Let's see if we can try to break it like we did earlier. If we were to make a spelling mistake here, the test is going to fail. Or, if we give data in the wrong format, the test is going to fail.