Confidently Ship Production React Apps
We want to make sure that when we ship new code, our users can use the application. The best way we've found to do that is to write automated tests that run before we deploy a new version of the app. But if our tests aren't doing exactly what the user will do, then how do we really know that things will work when users interact with our apps?
Let's rethink what it means to ship applications with confidence and what it takes to get there. With improved tools and practices, we can be more certain that what we're shipping to our users will work the way it's intended.
Additional resources: Slides, react-testing-library, Jest, jsdom, jest-dom
Transcript
Kent Dodds: [00:00] Hi there. My name is Kent C. Dodds. Today, we're going to be talking about Confident React with React Testing Library. This is some stuff about me. You can check this out later. I'll have a link to the slides at the end of this video. Let's jump right in.
[00:13] I like to start out my talks with expectations so that we're all on the same page. This talk is going to be a discussion of how to test web applications specifically with React to test those effectively. It's a little bit of a pitch of something that I've built that enables writing more maintainable tests. Just be prepared for that. I really believe in this, I built a library to enable that and encourage testing in this way.
[00:43] This is not going to be talking about functional or end to end testing specifically. Lots of the principles I talk about apply to that, but specifically I'm talking about React unit testing and component integration testing. Hopefully, this talk isn't boring.
[01:00] Let's go ahead and jump in and get started. My first question to you is why do we test our software? Do we test it because our boss told us to? Or, maybe we don't test it and so maybe the question should be why don't we test our software?
[01:16] There are actually quite a lot of reasons to test your software, and we're not going to dive deep into those. One of them is for work flow. If you really get into test driven development, then you can drive your software development by writing tests to drive that. That's a really good reason to write tests.
[01:34] What I want to focus on is confidence. That's what we're looking for when we're writing tests. Confidence that we can ship our code without having to manually go through and iterate on every single thing that we've built from the last release and make sure we didn't break anything. That's why we test our software, so we can be confident when shipping.
[01:57] How do we get the most confidence? What kinds of tests get us the most confidence? I'm a firm believer that the more your tests resemble the way your software is used, the more confidence they can give you. That was something that was said by somebody. His name is Kent C. Dodds, who is me, on Twitter a while back.
[02:18] I really believe that this is true. If you're testing things in a way that users could never possibly use your components in that way, then that test can't really give you a whole lot of confidence because I don't care that you can do that with your component. The user is never going to do that with your component, that doesn't really give me any confidence that it's going to work when the user tries to use it.
[02:41] With that in mind, what kinds of tests give us the most confidence? I would say manual tests, probably. If we're really interested in whether or not the user can do something specific, then we should just have the user do that specific thing and then we can feel pretty confident it's going to work when the user does that specific thing.
[03:01] The problem is that there are a lot of problems with manual testing. Not only is it very resource intensive or human intensive, but there's a huge opportunity for human error. I worked at a place a while back where most of the regression testing was manual. I'll be completely honest. When I went through some of these I would see something and be like, "Ah, that's probably fine. It was probably a fluke," and I would say that the test passed. It was not good and I know I'm not the only one who did that. Lots more bugs slip through when you're doing manual testing.
[03:41] How do we overcome these human shortcomings then with this in mind? Automation. Instead of having a human doing a repetitive task which humans are actually really bad at doing, we make a computer do it. With automation in mind, let's make sure we don't throw the baby out with the bath water. We want to automate things in a way that a manual tester would do it, as closely as possible because of that same reason that I mentioned before. The more your test resembles the way your software is used, the more confidence they can give you.
[04:16] With that in mind, we have this form here. There's a label for a username and a label for a password and then an input field for both of those. If I were a manual tester, how would I go about testing this registration form? I would fill in a username, fill in a password, go click on the submit button, and then verify that I was redirected to the home page and that I'm logged in, showing my user's display name.
[04:51] That's what you should be doing when you're thinking about how you're going to test something. Think about if I were a user and manually testing this page or manually testing this component specifically, how would I go about doing that?
[05:08] The state of the art for writing these kinds of tests for React components is Jest as your testing framework. If you've not used Jest before, I strongly recommend that you look into it. It's an amazing testing framework. It comes with all the nuts and bolts that you need, all wired together to make writing tests for JavaScript really just so much easier than it has been in the past. We're really, really happy with Jest everywhere that I've been.
[05:37] Then Jest under the hood actually uses an abstraction called JSDOM, because incidentally, Jest actually runs in Node, one of the reasons that makes Jest so fast is because it actually doesn't run in a browser. It doesn't have to pull up a full browser to run your tests. It'll actually run in Node.
[05:53] It uses JSDOM to simulate a DOM environment because we're writing tests for the DOM. Jest starts up JSDOM and then we need to have some way to render our React components into that DOM that's just being held onto in memory. Up until recently, the de facto standard for that was Enzyme.
[06:20] Let's go ahead and take a look at an example of this login component that we want to test and we'll see what it looks like testing that with Enzyme. First thing, implementation of this login. It's a login component with an onSubmit handler that it's going to call as a prop. It renders a form, a labeled input, two labeled inputs, and then a submit button.
[06:46] Let's take a look at that form. We've got it right here. It renders a form element and it has an onSubmit handler that gets the element values and calls the onSubmit handler, it renders the children which are labeled input. The button labeled input is just a label with an input. Pretty straightforward what that's all about. Then we have this get element value.
[07:11] What we really care about and what the user cares about is just this login right here. It doesn't really care about the implementation details of how that login is composed together. All of this could just be inline for all the user cares about. They don't really care at all. Our test should also not care about that fact. If we're a user looking at this form, all we really want to do is find the username input and fill out a username, and then find the password input and fill out a password, then find the submit button and click on that to submit the form.
[07:46] Here's what a test like this would look like from an outline perspective. We need to render the login with a fake submit handler so we can make assertions to make sure that the submit handler was called properly. Then, we'll get the user input and set that value to Chuck Norris, of course. Who else? Then, get the password input and set that value to I need no password because it's Chuck Norris. Password needs him.
[08:13] Then, we'll get the form and simulate a submit event, we know that that's wired up properly. Then, we can assert that our fake submit handler was called once and that it was called with the right stuff. That's the outline of the kind of test we're looking for. Here's what it would look like with Enzyme. We create our fake handle submit handler. It's just a Jest function, a mock function.
[08:42] Then we'll use Enzyme's mount function. We actually couldn't use Enzyme's shallow, which is what they generally recommend in Enzyme. That's because this labeled input is an implementation detail, but it's responsible for rendering the label and the input. If we shallow render this, we actually wouldn't be able to get a hold of the inputs to set the values. We have to mount it. As just an aside, never do shallow rendering. It's a terrible idea. That's a subject for another day.
[09:16] We're mounting this login with our fake submit handler. Then we need to find the username input. This is what you have to do with Enzyme. We take that wrapper. That's just a utilities object that has information about the thing that we've rendered. We're going to find the input. We want the first one because that's the username one, the first one. Then, we have to call this host node so we can get the DOM nodes associated with that input. Then, we need to get the instance because this host knows it's going to give us a wrapper. We want the actual DOM node.
[09:52] Then we'll set the value to Chuck Norris. We'll do something similar with the password input. We'll get at index one. We set the value to I need no password. Then we get our form and simulate a submit handler. Here, you'll notice we're finding the form with the display name of the form. This is something that Enzyme encourages you to do.
[10:20] I find it to be really frustrating because that is an implementation detail right there. In fact, this is also an implementation detail. The user doesn't look for the first input or the second input. The user looks for the input with the label username. We'll look at that a little bit later.
[10:40] When we are including implementation details in our tests, what's going to happen is we're going to change the implementation to a refactor of some kind, the end result is the same as far as the user is concerned, but the tests are going to break. Our tests weren't really testing the things that the user cares about.
[11:01] That makes people frustrated with testing. It's one of the reasons that lots of people don't want to test is because their tests are constantly breaking as they're making changes to their code. That's frustrating. By including these implementation details in our tests, we're just setting ourselves up for not liking testing, which is sad.
[11:21] In any case, we're going to make our assertions here to ensure that the handle submit is handled properly and this test is verifying the behavior.
[11:33] Let's take a look at an alternative approach. I got kind of frustrated with Enzyme because of some of these APIs it encourages. We can actually change change this test, refactor it a little bit to be a little bit nicer. But for the most part, there's not really an easy way to get DOM nodes to set values and things like that. That's just the way that you do it.
[11:57] Then we could probably find the form itself without using the display name of that form component. I was getting kind of frustrated with this. I was going to go teach a workshop about testing React. I was thinking, "OK, how am I going to teach people Enzyme?" When you make some sort of asynchronous request, you have to do wrapper.update to update the wrapper's references to the nodes and things like that. A bunch of stuff that I was just trying to figure out how I'm going to explain this to people.
[12:34] Then I realized I'm not using a lot of Enzyme. I'm just using it to render stuff. I don't use shallow. I don't often use the render method. I don't want to have to explain should you shallow, mount, or render. I didn't want to have to get into that. I thought, "What would happen if I just used React DOM and rendered things just like I do in my actual application?" That's what this same test looks like with React DOM. We're still going to create our submit handler, our fake function that's going to keep track of itself. Then we'll create an element, a div. This is just regular DOM APIs to get our container.
[13:13] Then, we'll use ReactDOM.render. We're going to render the same thing. We're just going to render that to our container. Then we'll get our inputs. This is regular DOM API, querySelectorAll, get all the inputs. We're not dealing with component instances or anything like that. Just give me the DOM node. I'm going to get those inputs. I'm going to get the username input.
[13:34] That's the first one. Set its value to Chuck Norris and then the password input is the next one. I need no password. Now, we're going to find the form and we'll use React's built in simulate testing helper, that's in React DOM, to simulate a submit on that form similar to what we were doing with Enzyme.
[13:56] Already right off the bat, this is to me a lot simpler, more straightforward kind of test than what we had with Enzyme. I was really happy with this. I started to build a couple utilities around this. This is what I came out with. You're still making your submit handler. You're still making those same assertions on that. It's what we're doing in here that's changing.
[14:20] We have this render method and that render is responsible for creating our container, for rendering it with React DOM. Then it gives us back a couple of utilities in an object. I'm just destructuring those utilities off. One of those things is actually the container that it's rendered to. That's just a regular DOM node.
[14:43] Then we also have this get by label text and get by text. Remember, if we're thinking about how does the user test this thing. The user is not looking for the first input and then the second input. They're looking for the input that has the label username and the input that has the label password. That's how they're going about this. With this utility, it enables that. Get by label text username. Find the input that has the label username and then set that value to Chuck Norris, same as the password. Then, we can just get the form and submit that form.
[15:20] This makes me feel a lot more comfortable because now if I refactor things and maybe I add another input here that is a checkbox that says remember me. Or if I add an extra input for maybe we make this a hybrid form so it has a first name input or something like that. Any refactors are not going to break this test. Changes to the experience could break this test.
[15:52] If we change this from username to email, then yes, this test will break. That's something that I want my test to catch. I want to make sure that I didn't typo username to usernames or anything like that. This test is going to verify that behavior for me. I feel a lot more comfortable with this kind of test.
[16:10] One thing that I don't feel super comfortable about though is this simulate functionality. The user isn't simulating submitting the form. In fact, the user doesn't think about submitting the form. What they're going to do is they're going to click on the submit button. Let's take a look at what that would take.
[16:32] Because React has event delegation, we actually have to get our elements into the DOM, into document body, so that any events that are fired in the DOM will bubble up to the document element and then that event delegation can take place. It's kind of annoying that it works that way, but that's the way that it works.
[16:56] I created this render into document helper that would render it to a container and then append that container into the document body so that events will bubble. Now, lots of this is all the same. The difference here though is now we're using this get by text helper and we're getting the submit button by text just like a user would. They're going to say, "OK, I filled out my username and password and now there's this button that says submit."
[17:23] There's also this button that says cancel. I don't want that one. We can refactor this, change all kinds of things, even change some of the experience to add a couple extra buttons and stuff. That's not going to impact our test at all because it wouldn't impact the user experience. They're going to be looking for the submit button to click on that and that's what we're testing.
[17:43] We're going to get by text submit, get that submit button. Then we're going to call click on it because it's a DOM node. When it's clicked, because we're wiring that submit button as a submit type, it's going to fire the on submit handler for our form and our handle submit is going to be called properly.
[18:05] One last thing we need to do is we need to clean things up because we've put something into the DOM. We need to unmount that component, take it out of the DOM. That's what the cleanup utility is there for.
[18:16] What you've been looking at is my library called React Testing Library. It's kind of an obvious name, but it is what it is and it provides a couple utilities that are really helpful for testing React. Those include the render method that you saw and simulate which you're using in tandem with render.
[18:40] Then there's a fire event utility. We saw that submit button we just called quick on the DOM node and there are a couple of other events that you can just call right on the DOM node like focus and things like that. There are some events that you can't just call like drag and drop experience or mouse overs.
[18:57] We have a fire event utility that allows you to fire any kind of event that the browser supports on a particular node. You can set custom properties for that. That's what fire event is useful for. If you want to use fire event, then you need to use render into document. If you're going to use render into document, you've got to clean up. Then wait and wait for element.
[19:22] These are the utilities. I think there's one more. There's a within, but you won't use that a whole lot. Wait and wait for element, we'll get a little preview of here in just a sec. With render, that's where the bulk of our usage will be. Render and render into document have the same kind of thing.
[19:42] You're going to have a container. It's going to give you back an object as a container. That's just the regular DOM node. Then you'll have this rerender function. If you want to rerender the component with different props to simulate that. Because when we're unit testing a component, the user is not normally going to just use that one component. They're going to be using an application that uses all these components.
[20:05] Some of those components are going to pass new props to that underlying component. We're going to rerender it just like the user would experience in the real browser. Then there's unmount, sometimes you have a component will unmount life cycle hook that you need to make sure that you're unregistering from some subscription or you're just cleaning up after yourself. The unmount is there to enable you to test that kind of experience.
[20:38] Then you have your queries. There's a get by label text, get by placeholder text, get by alt text to get images, get by text, just the text content of the element. Then there's this escape hatch where if these don't quite work for what you're looking for, then you can do get by test ID. That allows you to add a data-test ID attribute to any element on the page and you can select directly for that.
[21:12] It's a lot cleaner than using a CSS class that you're using and you're like, "Oh, I'm using this as CSS. I'll just use this as a test." Now, we change from button success to button danger or something and now your tests are breaking. That's super annoying. Having a get by test ID with data-test ID attributes makes things a lot easier and more direct. When you're looking at the component you know exactly what that attribute is for.
[21:43] That's render. Let's take a look at a couple other examples. Testing async in React can be kind of frustrating. The utilities provided by React Testing Library really simplified this a lot.
[21:58] We have a generic fetch component and here in the render method we're getting the data from state. If the data exists, then we're going to render this span with the data test ID of greeting, then we'll render that greeting. Otherwise we'll not render anything at all. Then we have this button on click and when you click on that it's going to call the fetch to make an HTTP get call to this.props.url. Then it will set state when that's finished. That's our async example. Also on component did update if the URL changes, then we'll do another fetch.
[22:38] Let's take a look at how you would test a component like this. First of all, you're going to make this test async. I'm so grateful for async await. It really simplified these kinds of tests. Then, we have this Axios mock that we've set up with Jest mocking capabilities. We're going to mock resolve value once on the get HTTP call. Axios.get, we're going to mock that. It will resolve to an object that has data with an object greeting, "Hello there." That's what we're going to be getting here. We're going to set the data and then get data.greeting.
[23:19] Then, we'll set our URL to /greeting and then render fetch with the URL. Then because we're using render, we're going to use simulate, simulate click on get by text, the fetch button. Just like the user would, they're going to click on that fetch button and then if the user were manually testing this, they're going to be looking at the screen. They'll click on the thing and then they're going to wait for the greeting to show up.
[23:47] After that's happened, then they can make some assertions about that greeting having loaded. They're not going to check the promise and see when it's resolve or anything. They're looking at the DOM and that's how they interact with your component. That's how your tests should interact with your component as well.
[24:06] We're going to await this wait for element. This wait for element accepts a function and it should return an element. As soon as that function doesn't throw an error and an actual element is returned, that's when we can proceed. That's when the promise that's returned from wait for element is resolved.
[24:25] We're going to get by test ID greeting. As soon as this response has resolved, we've set state and it rerenders. Then data is now going to be defined. We set state for that. Our greeting is going to be displayed. We wait until that all happens. We'll get our greeting node. Then, we can assert that the Axios mock was called properly.
[24:51] We can assert that the greeting node has text content "Hello there." This to have text content is provided by a handy little library for testing called Jest DOM. Lots of those assertions were originally baked into React Testing Library and they were extracted to Jest DOM. You can use it with anything in Jest. It's pretty cool.
[25:14] Then also, I just threw this in there because lots of people seem to really like snapshots with Jest. If you are one of those people, then you can actually take a snapshot of a DOM node. It will get formatted. It looks awesome. In fact, I like it a lot better than snap shotting in Enzyme wrapper or snap shotting a React test renderer to JSON thing because it excludes any implementation details.
[25:44] Those will generally include all the props that you're getting passed, the names of the components that are being rendered. Any time you change the name, you change the props, any of those things, you're going to get a snapshot failure. That makes members of the team hit the U key and you just kind of get bored of updating this all the time, so you don't review it very carefully. By excluding implementation details from your snapshots, that makes your snapshots actually a lot more valuable.
[26:16] That is it for me, for Confident React. This is a link to the slides. I hope that was helpful to you. I'll see you around on the Internet.
[26:26] Bye.