In this lesson, we’ll create a utility function to add items to a list, avoiding mutation by returning a new array. We’ll verify this behavior by creating unit tests with Jest.
[00:00] Right now, this list of todos is static. In order to start making it a little more dynamic, we need a way to add new todos to the list. For that, we'll create a helper function. Since Create React App comes preconfigured with Jest for testing, we'll build this helper function in a test driven style.
[00:17] Let's start by adding a lib directory under source.
[00:19] I'm going to add a file to lib, which I'll call todo helpers.js, and I'll add a second file to lib called todo helpers.test.js. At the top of the todo helpers.test file, I'm going to add an import to add todo from todo helpers. With that in place, I'm going to jump into the terminal and run npm test to fire up Jest.
[01:07] You see that I have a failing test, and that's because my test suite must contain at least one test. I'm just going to paste in some test code that I have prepared, and let's take a look at it.
[01:20] This test file is a pretty standard structure, often referred to as Arrange, Act, Assert.
[01:26] If we look at this, we have the Arrange portion of our test where we have a start todos variable that has an array with two todo objects in it, a new todo, which is going to be the object that we're going to try to add to our list. Then a const called expected that represents what we want the list to look like at the end of running add todo.
[01:47] The second section is referred to as Act, and this is where we actually call the code that we're trying to test. Here we're assigning whatever comes out of add todo to result, and we're passing it in start todos and new todo.
[02:01] Then finally, we have the Assert part of the test, where we actually set our expectation. This is what's going to determine if the test passes or fails.
[02:10] With our test in place, I'm going to save that, Jest will automatically rerun our test. This time we're going to get a type error because add todo is not a function. Because we haven't defined it yet.
[02:20] Let's go to todo helpers, and we'll add our add todo function.
[02:23] For that, I'm going to say export const add todo, and I'm going to set that to equal a function. Now that our function's defined, we can save the file.
[02:34] Jest will rerun, and we're still failing, but now we're failing for a different reason. We expected an array and we received undefined. We can scroll up a little bit here and we'll see that it actually expected a specific value, and that's based on what we assigned here to expect it.
[02:52] Let's make this test pass.
[02:54] I'm going to jump into todo helpers, and if you remember from our test, where we call add todo, we're passing in the array, which we'll just refer to as list here and then we're passing in the new object that we want added to the list.
[03:07] Since I have an array, I can use array methods, so let's say I want to call list.push and I want to push that new item into the array, then I want to return the list. I save that, my test reruns and everything's passing.
[03:23] I'm going to go back to todo helper, and I'm going to add a second test. Going to scroll down and I'll paste this in, and we'll see that this test is called add todo should not mutate the existing todo array.
[03:35] I have the same setup with start todos and new todo and my expected array. I'm calling add todo with the same arguments, getting the result back. But this time I'm checking to make sure that start todos and result are not referring to the same array.
[03:52] If I save this, we'll see that a test fails, and the problem here is that by calling list.push in this function, I'm mutating the original array. What I need to do is get a new array back that has the items from the first array plus the new object.
[04:10] We can accomplish this by updating our code here, and using concat rather than push. We can just return the result of that, because concat will add the new item into a new array and return it. I'll save that and now we have two passing tests.
[04:28] Now that we have these unit tests in place, we can make all kinds of changes to this with confidence that if we change the behavior in such a way that our paths fail, it'll tell us right away.
[04:38] I'm just going to do a little bit of refactoring, so I'm going to get rid of these curly braces and that return and I can make this a one line function. I'll save it, tests run again, and now you know I'm good.
[04:49] Now let's say I wanted to change the way I'm doing this. Instead of using concat, I want to use the spread operator.
[04:55] I'm just going to do this, and I'm going to say, I want the list, including the new item at the end of the list, and I'll save it. Because my tests pass again, I know that this is a valid replacement that updates my array giving me the new item and also doesn't mutate the existing array.
Anthony,
Thanks for the feedback. I'll leave some reasons here in case anybody else is looking for it.
Immutable data structures and pure functions make code easier to test and cut down on unexpected behavior that can arise when data is changed in place. In the case of React specifically, immutable data also makes it easier to optimize rendering passes by comparing object references in shouldComponentUpdate
or when using React.PureComponent
Hope this helps clear this up.
For some reason, npm test
doesn't seem to be working for me. I just spent quite a bit of time trying to re-install the project, and I even tried to run it in seperate create-react-app application's i have built and for some reasons it gets stuck at Determining test suites to run...
. Any feedback would be appreciated :)
Thomas,
I can't reproduce this, so I'm guessing, but this sounds like it could be a problem with watchman
. Maybe check out this issue discussion and try addressing watchman directly.
https://github.com/facebook/jest/issues/1767
I'd be curious to know is this fixes it for you. I hope this helps!
That solved it for me. I think it'd be worth including this in video 1.
I like that you stick to not mutating the original object, but maybe a small comment on why that's important or the reasoning behind it would be helpful for others as well.
I was thinking the exact same thing; thanks for the explanation Andrew!
I like that you stick to not mutating the original object, but maybe a small comment on why that's important or the reasoning behind it would be helpful for others as well.