When using Redux, we can test that our application state changes are working by testing that dispatching actions to the store creates our expected output. In this lesson we will run a few realistic actions back to back (as if the user is using the app) and then test that the state tree looks as we expect it to. These types of tests that ensure all of your redux logic is working as expected give you a lot of value for not too much effort (they test your entire app's state in one big swoop). You may also find it useful to add more granular/individual tests for your reducers and/or actions, which we will cover in other lessons in this course.
NOTE: This lesson assumes you have used Redux. If you are new to Redux, it is recommended that you first watch the Getting Started With Redux course.
[00:00] Here we have an app that lets us add quotes to a list, and then once we have quotes in the list we can do a few things with them like remove the quote, or like, or unlike the quote. We can also change the theme of our entire quote list. All of these state changes are managed in our redux store, so we'd like to write some tests now to make sure our store is working as expected. Here we have a test file for a redux store, and the first thing we need to do is import store, next I'm going to import my assertion library.
[00:35] I'm going to create a describe block for the store, and then I will say that is should work with a series of actions. Now the first action that I'd like to dispatch is going to be to add a new quote to our state tree. So we need to do store.dispatch, and then pass that an action.
[00:57] The action type will be addQuoteById, and then for the action's data I'm going to be using the payload convention of flux, so this payload object wrapper is not required by Redux, I've chosen to use it here just to follow the flux convention, we could have just as easily placed these properties directly on the root object. Either way, our action now contains the quote text, the author, the quote ID, and the like count.
[01:25] Now to write my assertion I'm going to go down and create a variable called actual, and we're going to get the actual value of the state tree by calling store.getState, then we're going to create an expected variable which is going to hold what our state tree should look like, what we expect to look like. We'll fill this in in just a minute, but first let's write our assertion that we expect the actual value to equal the expected value. Now let's go write out what we expect our state tree to look like after we dispatch the actions above.
[02:02] At the root of our state tree we're going to expect there to be a quotes array. Inside of our quotes array we're going to expect this single quote object that we dispatched earlier. That's all we need for our first test. Now I want to dispatch more actions and make sure that our expected state tree gets updated correctly with the different actions back to back.
[02:23] So the first thing I'm going to do to make this a little bit easier to work with. Instead of calling dispatch for every action, let's just create an array of actions that we can iterate over and call dispatch on. So on line seven, I'm going to create a new variable called actions that will be an array. Now let's go grab our action, we'll place it inside of our actions array. Now we no longer need line 18, so let's get rid of that.
[02:52] Now we just need to say that for each of our actions, we're going to pass in the action object, and then we're going to use the dispatch method on store to dispatch that action. Now we're ready to add some more actions to our actions array. I'm going to go up to line 16 and we'll add a comma here, and then a new object. Let's add another quote to our state, so we'll see type of addQuoteById, just like before. Then we'll add our payload.
[03:22] Now we have just like our first action, we have another quote, but the difference here is that it adds different text, different author, as well as an ID that's been incremented by 1, and this one has 0like counts to start out with. Now let's go back down to our expected value, and it looks like I made a mistake here. I actually need to have these quotes wrapped in an object. So now we can add our new quote to our expected state tree.
[03:54] Now we have two quotes in our expected state tree, and the next action I would like to dispatch is going to be to remove a quote. Let's actually remove the first quote that we added, and I'm just going to remove lines 33 through 38, and now let's go up and add that action to our actions array. I'm going to add an object to this array, and type is going to be removeQuoteById. Now for our payload all we need to do is pass in an object that has an ID and we just remove from our expected state tree the quote with the ID of 1.
[04:35] Now that we've tested adding quotes and removing quotes, let's test liking and unliking quotes. So I'm going to add a new action object, and the type is going to be likeQuoteById, and the payload is going to be of the ID of the quote that we want to like. So the only quote that we're expecting in our state tree right now since we've removed number 1 is number 2. Let's like that quote.
[05:01] To be a little bit more thorough, I'm going to copy and paste this action so that it happens twice, so we expect two likes to be added to quote 2. Now let's go down to our expected output of our state tree, and on line 49 instead of like count being 0because we just dispatched two likeQuoteById actions, we expect this to be 2. Now that we've tested liking quotes, let's create an action in our actions array for unliking quotes.
[05:29] I'm going to go back to line 37 where we added our last action object, and I'm going to copy this likeQuoteById action. We'll add a comma between our objects, and then paste it. Now I'm just going to change the like to be unlike. Now that we've added this unlike action, let's go back down to our expected output and on line 53 with our like count let's change this back down to 1. Let's add one more action to our actions array.
[05:59] I'll go back up to line 41, I'm going to add a new object here, for the type let's use updateThemeColor, and for the payload let's pass in the color of 777. That final action we're going to expect that it will update our state tree by adding next to quotes, we're going to have a theme object and we're going to expect the color to be updated to 777. Let's go back to the top and review what we've done here. We've created an actions array, and inside of that we're passing in action objects.
[06:42] So the first one adds a quote by Mark Twain, then let's go down a little bit and we can see that our second object also adds a quote which is by Abraham Lincoln. Then our third action removes the Mark Twain quote. The fourth action likes the Abraham Lincoln quote, the fifth action likes the Abraham Lincoln quote as well. The sixth action unlikes the Abraham Lincoln quote, then finally the seventh action updates the theme color.
[07:11] What we do is we take this array of actions, and we iterate over each of them, and we dispatch that action. Next, we get the state of our store, and then we create a variable to store what we expect our state to look like. So we added two quotes and removed one of them, so only the second one is left here. Then we liked and unliked that quote, so this is the value we expect in the end. Then we changed the theme color.
[07:39] Finally at the bottom we have our assertion that expect our actual state to be, our expected state. During this lesson I didn't run any of these tests to save time, but I would definitely recommend running the tests each time you make a change in your own code.
It seems like a better approach may be to test your action creators as pure functions returning state. What do you think?
Kamuela, you could do that. I personally use the store tests to test how all of the actions work back-to-back (to ensure the entire app is working as expected) - as is done in this lesson. Then, I test individual state updates in the reducer tests - as is done in the "React Testing - Redux Reducers" lesson: https://egghead.io/lessons/react-testing-redux-reducers?series=react-testing-cookbook.
There are a lot of different ways to go about testing; I don't think there is a single "better approach". In my opinion, as long as you are testing things that are important to you (things you want to ensure don't break in your application without you knowing about it), that's all that matters :)
Thanks for watching and for your input!
I am going thru your testing videos on egghead, and browsing your code.
I noticed your "state" is split into various folders, and I am curious why.
which references outside of it, into "other" stores:
So, you have a "store" that sits in "state" folder -- that then references "state" in other outside directories.
The reason I ask is because I am wrapping my head around Redux right now, and want to make sure I am understanding convention.
To be clear, a Redux app (including this one) only has one store; in this course, the store is located in
src/state/store.js. However, the store can compose together multiple reducers. In small Redux apps, there might be only one reducer (as shown in the
counter-vanilla official Redux example). As the app grows, it makes sense to add more reducers. You can put these reducers wherever you want as long as you compose them together in a main reducer, then use that main reducer in your store correctly. For example, the
counter official Redux example has a "reducers" folder with all of the reducers in it. In my
favorite-quotes repo for this course, I place each reducer with its own feature folder (
src/home/theme etc.); I personally like to organize my code by features instead of by type using a pattern like https://gist.github.com/ryanflorence/daafb1e3cb8ad740b346.
If you have questions about how Redux Reducers and the store work, the official Redux docs are awesome: http://redux.js.org/
Hope this helps :)