Kent walks us through the internals of dom-testing-library and react-testing-library.
There is a lot of set-up and tear down when testing UI components. These two libraries take away a lot of the work required to test React components well.
Jest is commonly used in tandem with react-testing-library to assert results of your components. Jest simulates most browser apis with jsdom. react-testing-library helps you get values out of your components the same way your end users will.
Links from the show:
[00:22] React-testing-library, I pretty much understand what it does, but I've never used it. I'm curious about it. Today, we're not really going to get into...if I understand right, this isn't a react-testing-library tutorial.
[00:36] Kent C, Dodds: No.
Joel: [00:37] We're going to get in and understand what's going on under the hood, that makes react-testing-library. React-testing-library, it's not the only testing library, right? It feels, like to me, it started like a group of these commonly named libraries. There's dom-testing-library. I've seen others as well.
[00:59] Maybe you can describe what a react-testing-library is and how that ecosystem works and then launch into your screenshare when you're ready.
Kent: [01:06] Sure. Before I get into the screenshare, I'll just talk about the history of the library. It actually started just about a year ago now when I was getting ready to do a workshop about testing React applications.
[01:24] When I do that I like to take a moment to explain the abstractions that we're using and even have exercises, so that you deal with the raw APIs, and then you'll understand what the abstractions are doing a little bit better.
[01:41] As I was going through that I said, OK, let's write a test that's just a React test that just uses react-dom to render. It fires events, it uses dispatch on element nodes and stuff like that.
[01:58] Then I switched over to using ReactTestUtils and Simulate, and then we moved over to Enzyme, which at the time was the de facto standard and is still with Inertia wildly more downloaded than react-testing-library currently.
[02:20] That jump between react-dom and Enzyme was really big. At the same, I also needed to take some time to say now here's the list of all the available things you can do. I'm going to show you the ones that you should never ever do, ever. There's never a good situation to do these things.
[02:41] I didn't feel super great about that. I thought what I have done was built an abstraction on top of Enzyme that only allowed for the things that I liked, so that I don't have to have that conversation. I realized that I didn't need Enzyme. I just could build it right on top of react-dom, and then create a react-testing-library.
[03:01] Two weeks later, I went and gave my workshop, and told everybody to use this brand new library that nobody had ever used before. Luckily, it worked out pretty well. That's where react-testing-library came from. Over time, in a matter of a month or so, I realized that most of react-testing-library was actually not React specific.
[03:23] We could get benefits of sharing with Preact, and with View, and Angular, and everything else by taking those things that aren't React-specific and putting them into a separate library, That's where dom-testing-library came from. Once dom-testing-library existed, then people were able to start building on top of that.
[03:41] Now we've got a whole bunch of things. That's the history of the library. Now, react-testing-library is endorsed by the React team. They recommend people who use react-testing-library to test their React applications, which is pretty sweet. That's where we stand today.
Joel: [03:59] It has to feel pretty good, man.
Kent: [04:01] Yeah. It's pretty sweet.
Joel: [04:03] You just make something to scratch your own itch, and then over time, and with community, now you have a tool that helps a lot of folks. Awesome.
Kent: [04:10] Yeah. For real. Are we good to share my screen here?
Joel: [04:16] Yeah. I think you can just push the button and go for it.
Kent: [04:21] Right now we're on the dom-testing-library website or testing-library.com. By the way, the name react-testing-library, came when I was thinking...I think I was going to call it React Testing Assistant or React TA. I was like, "Oh, what a brilliant name." I tweeted that I had finally come up with a name for my react-testing-library.
[04:50] Ryan Florence replied to that tweet and said, "What is it? React-testing-library? Lol." I thought that was great. I changed it and grabbed that on npm. Now we have testing-library.com. This will take you to the dom-testing landing page, with an explanation on how to get things going.
[05:11] We have Facebook open source on our list of users, which I think is pretty legit. I think they're going to add Facebook itself as well. I know several product teams at Facebook who are using react-testing-library which is sweet.
[05:24] Then we've got a bunch of different community-built packages for dom-testing-library, which is pretty sweet. React-testing-library is one of those, and then we've got all the docs in here and stuff.
[05:38] Like you said early on, this is not a session about how to use react-testing-library or anything, this is more on how does react-testing-library work. We're going to be mostly looking at code and that kind of thing. To get us to that spot, I bootstrapped a React create-react-app application, that doesn't really do anything, just renders, "Hello, World!"
[06:07] I'm going to show you how to build basically react-testing-library, at least the fundamentals, the very beginnings. I think that's where things get interesting, it's like let's see...
Joel: [06:20] This is build your own react-testing-library.
Kent: [06:22] Yeah, exactly. I'm going to go ahead and add a file called test, and we'll do counter.js. Counter is probably my favorite example of a component, so I've actually got a snippet for a React counter, and of course we're going to be using hooks here.
[06:43] We've got react from react. Cool, so we've got our counter. In fact, you know what, to make things even less confusing, I'm going to move this to counter.js. This is where it would typically live in your application, and then you'd export default counter. Then in your test file, what are you going to do?
[07:03] You're going to need to import counter from counter, and we're going to be rendering React elements, so import react from react. Then we're going to write a test. We'll just use Jest for our testing framework here, we'll just use the global test here. Our test is that it increments when clicked.
Joel: [07:30] I'm just going to...sorry. What just happened? You said global.js, just to get some bearings on where that comes from.
Joel: [08:01] Nice.
Kent: [08:01] because here I'll show you how to build your own jest, which is really good. People love this.
Joel: [08:09] Basically, we're using create-react-app and it understands that folder exists, and will run those tests and test is a global function that we can call at any point if were writing something following the convention of __test and putting those files in there? Is that?
Kent: [08:27] Yes. Exactly right.
Joel: [08:28] OK, that's all I wanted to know.
Kent: [08:29] No, that's a great question. Keep those interruptions coming, that's helpful.
Joel: [08:35] OK.
Kent: [08:37] I think that's actually a really important thing to highlight as well. People get this confused a lot, where they say, does react-testing-library replace Jest? A question like that is understandable for people who don't have a solid understanding of what these tools are, but it reveals that you don't understand what these tools are.
[09:00] Jest is a testing framework that will run your tests and it also provides APIs for you, for writing tests. React-testing-library is a library specific to React that allows you to render React components and traverse those React components, or what you've rendered, and then you can make some assertions on that.
Joel: [09:23] It's UI testing?
Kent: [09:25] Yes.
Joel: [09:26] Component testing?
Kent: [09:28] Yep, exactly. Component testing. We want to render this counter, all this counter does -. I kind of skipped over this -. it has some state for the count, it has an increment thing that will update that count, and then it renders a button. Every time you click on the button it's going to increment that count. That's all that this thing really does.
[09:47] What we want to test is that when I render it in the first place it should initialize to , then when I click on that, the count should be incremented and I should see 1 at this point. Let's go ahead and do that. How do we render stuff in React? Well, we can look at our index.js here, and this is how we render stuff in React. This is how we get stuff into the DOM.
[10:10] That's the same thing I'm going to do in my test. I'm going to say react-dom. We're going to need to import ReactDOM from react-dom. Instead of rendering my app, I'll just render the one component that I care about.
[10:23] Instead of rendering it to some global thing that's already been set up in my app, I'm running in my test. Where, in my app, I have access to this root element, that's because it's in my index.html. There it is right there. That works for my app, but it doesn't work for my test because I'm not loading things in this index.html.
[10:45] What I'm going to do is I'm going to create a div. I'm going to say, "document.createElement('div')." We're going to put this div into our DOM. We'll say, "document.body.appendChild(div)." We'll render our counter to our div.
[11:09] Let's go ahead and I'm going to console.log(document.body.innerHTML) so we can see what things looks like at this point. We'll say, "npm run test." That'll start Jest, the Jest testing framework, in watch mode. There we go. There's my console.log. Currently, the body has a div inside of it. Inside of that is my button.
[11:39] From there, I can start making assertions. Actually, what would be better probably is to get access to the button so I can get what its text content is. Let's get our button. That'll equal document.body.querySelector('button').
[11:57] Now we've got our button node. We can expect that the button text content to be zero. Text content is always going to be a string. Even though we're rendering the number zero, it's going to render it out to a string.
Joel: [12:12] I have a question.
Kent: [12:14] Yeah.
Joel: [12:14] Where does document come from in this case? We're not running our index. Is that just something that Jest is doing under the hood for us?
Kent: [12:21] Yeah. Good question. Yes, that's exactly what it is. Jest, it actually all runs in Node. Jest uses a package called jsdom that will simulate all the browser APIs in Node. It can't do all of them.
[12:36] Stuff like layout and drag-and-drop and stuff like that, jsdom isn't really well-suited for, but I'd say 90 percent of the things you're going to do, maybe 99 percent of the things you're going to do can work in jsdom-land. That's where document.body comes from. Jest automatic...
Joel: [12:53] It just works...
Kent: [12:54] It just works.
Joel: [12:55] if you're in a create-react-app environment. I'm assuming that's doing a lot of the configuration for us.
Kent: [13:00] Yup, exactly. Jest actually, out of the box, by itself, will do most of that stuff for us, like set up the jsdom environment and stuff, which is pretty cool. Create-react-app is a super-awesome place to start for this kind of thing. Great question.
[13:19] We have our assertion here. We can actually break this. This is your test of what a real test is, is if you can go into the source code and break it, like break the component. If your test fails, then your test is useful.
[13:34] I'm going to say, "React.useState." We'll initialize that state to one. Now my test should fail. It does because I expected zero to equal one. It's one because I initialized it to one. We are good and set.
[13:50] Now let's go ahead and click on that button and make another assertion. There are a couple ways to do this. I'm going to do this the easy way first. We'll say, "button.click." I'm going to console.log(document.body.innerHTML). Now we see that the button, that text content, it's been incremented, which is sweet. Now I can make an assertion again to say that that is one.
[14:21] We have a good, solid test. This covers us 100 percent for this component, all of the use cases that this component supports. We're doing this all just with regular React APIs and DOM APIs. We're not doing anything special here to make all of this stuff work.
[14:37] The fundamental idea behind react-testing-library is to be as close to this as possible. Conceptually, most of your React tests are going to involve some of the similar things. Where you created a development, that development needs to go into the document. You're going to use ReactDOM to render to that div.
[15:03] You can make selections on the document for the elements that you've rendered, make some assertions on those elements, interact with those elements, and make some assertions based off of those interactions. That's the fundamental idea behind all testing of React components.
Joel: [15:21] If we didn't build react-testing-library and we had a stack of 200 of these tests, they'd all have this same boilerplate setup. That's the first indicator that we could probably do better, right?
Kent: [15:34] Yeah, exactly. That's exactly right. Every one of them is going to have that. What's more is if we were to add another test in here -- we'll say it's exactly the same as the first one -- the second test is actually going to fail.
[15:49] The reason that happens is because we never cleaned up after this test. Before we even start anything, if we go ahead and console.log(document.body.innerHTML), we're going to see that we already have a button in there that has the contents of one.
[16:11] Once we've rendered this next counter, we're now going to have two in there. When we say, "Hey, querySelector('button')," we're going to get the first one that we'd already run our test on. That's why the second one fails. We've got some cleanup that we need to do as well. We can do that...
Joel: [16:28] This is like testing basics to me. If I'm thinking about unit tests or testing my application, global state across tests is one of the big no-nos. In top five things not to do is have global state that carries over between tests. That's just bad.
Kent: [16:46] Yup, exactly. I'm not going to get too far into all of this stuff. Suffice it to say that there's setup that's going to be common across all of our tests and there's teardown that will need to be common across all of our tests. That is what react-testing-library embodies, that setup and that teardown.
[17:08] Actually, there are two other things that react-testing-library does for you that makes things a lot better. That is this querySelector for a button. What if we change that to a link or if for some weird reason we need to use a div or something? What if we're rendering multiple buttons?
[17:30] Having our test just looking for a random button is a little bit of an implementation detail. In this simple example, it's not really because all this thing is rendering is a button in the first place.
[17:42] If we have a form where we say, "Find me the first input and fill that in. Find me the second input. Fill that in," now our tests are relying on the order of those inputs rather than what those inputs actually represent, what they're labeled to be.
Joel: [17:54] This is why UI testing is a pain in the butt, and nobody wants to do it, right? At the end of the day, because all this stuff, and you have to manage stuff. What if something, are they brittle? All that stuff is why, traditionally speaking, people have been like, "You just don't even test it."
[18:10] Test your logic, services, or whatever, and we're not even going to test UI, because of this stuff, I would think.
Kent: [18:16] Yep.
Joel: [18:17] That's why I never have.
Kent: [18:20] That's exactly the sentiment that I've heard from a lot of people, and it makes a lot of sense. It's not that testing UI isn't valuable, it's that the return on investment is too low.
Joel: [18:34] We all have PTSD from Selenium.
Kent: [18:37] That, too. If we can...
Joel: [18:40] Selenium's a great product. You know, whatever, if you want.
Kent: [18:43] If we can lower our investment and increase our return, then it suddenly becomes more valuable. Another really important piece to react-testing-library is some enhanced queries that allow your tests to be separated from the implementation of your component, so that instead of looking for the first input, you're actually looking for the input that has a specific label.
[19:11] Like username, email, first name, or whatever. That way, as your design changes, as your implementation changes, so long as your component is still accessible from an accessibility standpoint, then react-testing-library, your tests should continue to pass.
[19:33] That's another really important piece of react-testing-library. It's really the mantra of dom-testing-library in general.
Joel: [19:40] Is it enforcing Ally-style accessibility in some ways?
Kent: [19:46] Yes.
Joel: [19:46] To properly test your app, it really needs to be accessible, otherwise you're not even going to be able to test it.
Kent: [19:51] Exactly.
Joel: [19:52] We get to double dip, right?
Kent: [19:53] Yes, yes. People are loving that aspect of it. I love it, too. When people file an issue and say, "Hey, I can't get my input," and I say, "Oh, it's because you have a typo in the ID in the HTML4," then we just not only made it so they could test their app, but also made their app more accessible. Just by default, it's just the way it works, which is pretty cool.
[20:18] The big mantra of the testing libraries is the more your tests resemble the way your software is used, the more confidence they can give you. That's exactly what we're trying to do by providing you with these queries that I'll show you here in a sec, as well as some other APIs, is to make it so that your tests look exactly the way that it's used.
[20:41] I'll just take a quick break and talk about that a little bit more. Your components really have two users. This counter component right here, there are two users. There's the developer user. That's me right here in my test.
[20:55] I'm rendering this counter component. I'm the developer user who cares about what the API looks like. I want to make sure, I don't really maybe care how things are done on the inside, what it's rendering, all of that stuff.
[21:09] I care that when I pass an onChange handler to my form component that my onChange handler is going to get called in a certain way when changes are made. The second user is the end user who's going to be interacting with whatever's rendered from that component.
[21:24] You have these two users that your tests should be resembling and that the component author needs to take into consideration as they're making changes. We're not going to be changing test willy-nilly from username to email, because that'll change user expectation.
[21:43] Now, my username actually has to have an @ symbol in it. I'm not going to be changing the props from handleChange to onChange just willy-nilly. I'll have to communicate that to my developer users. The problem with having tests that don't resemble the way that your software is used is that this adds a third user that the component author has to take into consideration.
[22:10] That third user is the tester. Nobody in the business cares about the tester. If the tester can't do their job, the business will continue to run just smoothly. Taking time to build your things just to satisfy the tester is a big waste of time.
[22:30] If instead, you can have the tester use the same API that you are exposing for the user and the developer that you already have to care about, then that's one less user to concern yourself with, a little less time to be wasting making special considerations for that test user, that third user that nobody really cares about.
[22:57] That's the basic idea behind this mantra. The more your tests resemble the way your software is already used, the more comments they can give you. Cool.
[23:10] The last thing, and then I'm going to start showing you what react-testing-library really does for you and how it's implemented is this button.click. We just lucked out a little bit that buttons have a click method, but there are lots of events that our elements don't have, like mouseover, for example.
[23:33] We need to be able to fire special events on these DOM nodes that we're interacting with. There's a way to do that. You can say button.dispatchEvent, new mouse event. It's a click event. I'm pretty sure that actually should work.
[23:52] Let's get rid of this other broken test. I honestly, see I'm using the utility. I don't remember what this would be. Oh, I think you actually have to say bubbles true. Yep, there we go. This is why react-testing-library is so helpful, so we don't have to remember that stuff.
[24:12] Sorry, Joel, did you have something you wanted to ask?
Joel: [24:15] I just said ouch.
Kent: [24:16] Yeah.
Joel: [24:16] To remember that every time, like bubbles is true. I'd be surprised, probably, if a lot of people even knew what that was doing.
Kent: [24:24] Yeah, exactly. The reason this matters is for event delegation, because React actually just sets event handlers at the top of the document on document.body. Then it'll pass event handlers to where they're supposed to go, which is actually annoying.
[24:43] It's also the reason why we have to put the div in the body. Anyway, react-testingl-ibrary takes care of all of this for you by instead of having you do this, it gives you a utility called fireEvent, where you can say .click on the button, and it handles all that weird stuff for you.
[25:04] You have everything that you could dream of on here, like mouse enter or whatever, whatever you want to do on there. Those are the things. I'll just quickly review, and then we can get into actual how is this stuff implemented.
[25:19] The setup stuff, the teardown stuff, which is going to take a little bit of code, so I'm not going to show you. Tear down. Then some queries to make your tests resemble the way your software is used. Then being able to dispatch events on these elements in a way that is understandable by React.
[25:42] Those are the things that react-testing-library does for you. There are a couple others that we might get into, like rerendering your component and things like that, but those are the fundamentals right there. Cool. Let's jump into react-testing-library, what this thing even is.
[26:02] The react-testing-library repository's on my GitHub, kentcdodds/react-testing-library. I want to first just mention, we have 71 people who have contributed to react-testing-library. There's also dom-testing-library as well, and that itself has a whole bunch of contributors also, which is pretty sweet.
[26:26] I'm really happy with how enthusiastic the community has been around this project. There are a couple of examples and things in the repo, but most of the docs are on testinglibrary.com. Without any further ado, let's jump into the source code.
Joel: [26:49] There was one question, just before we get into the source code, which I'm really excited about. The idea of, is this different? We're testing the DOM. We're testing the UI. Is this different than what we would do with Cypress in an end-to-end test? Why bother doing all this type-it-up testing, versus click testing, or versus end-to-end testing with an automation tool?
Kent: [27:12] That's a great and pretty big question. Actually, of course, as it's pretty typical for me, I have a blog post about this -- I have a blog post about just about everything -- where I compare static unit integration and end-to-end testing, and explain where each of these are useful.
[27:35] I would strongly recommend anybody who's interested to go look at that. To give a quick answer to that question, the reason that you would use react-testing-library instead of Cypress...By the way, there's actually a cypress-testing-library.
[27:54] If you like some of these utilities, you can have them in both worlds. The reason that you'd do this is because envision the end-to-end testing as you're trying to cover a wall with paint. That's your coverage.
[28:12] The way the end-to-end tests do this is with a giant bucket of paint. You just throw a bucket of paint against the wall, and you hope that you get most of it. Lots of times, you do. You get a lot of coverage doing that.
[28:23] You're never really going to get the corners really well, especially if you're trying to not get the ceiling, the other walls, and stuff. You'll never successfully get those corners. With those, you're going to need a smaller brush.
[28:37] With some of these, if you've got to go around a window or something, you'll need an even smaller brush. The idea is, with these different layers of testing, they help you catch different edge cases at all these levels.
[28:51] Sure, you could get 100 percent code coverage with your end-to-end tests, but those are going to remarkably slower than just focusing on the general cases for end-to-end tests, and then getting more specific on lower level tests. I hope that made sense.
Joel: [29:09] You would have more of these. We would be able to write more of these. They'd run a lot quicker, because we're not doing actual firing up a browser and navigating through it. They are going to be generally less brittle than our end-to-end tests.
[29:22] They're going to be a larger portion of our testing coverage. They have similar use cases, but this is just going to be able to go broader and be more pinpoint-specific as to what we test.
Kent: [29:36] Exactly. Thanks for summing that up for me. That was good. Cool. Let's go ahead, and we'll take a look at index.js. This is where we enter into react-testing-library. [inaudible] that up even further.
[29:50] We got React, we've got ReactDOM, and we're pulling in a couple things from dom-testing-library, a couple of useful things there. We'll dive into those here in a little bit. We're also pulling this act thing from act-compat-net. We'll probably talk about that toward the end here as well.
[30:13] The render function is the main export from react-testing-library. It's the thing that you're going to be interacting with almost every time. Render accepts a UI. By UI, this is a React element. It's like myCounter.
[30:30] This converts to React.createElement counter. What that returns is React element. It's like a dom descriptor or an element descriptor. It basically just describes what the UI should look like, so we call that UI.
[30:51] Then you have some options here. You can provide a container, the base element where your query should be based off of, any custom queries you want, whether you want to hydrate rather than render, if you're testing server rendering capabilities, and then a wrapper component, if that's something that you need for context or something like that.
[31:14] Fundamentally, most of the time, all you're going to provide is this UI. If there is no container provided, then we're going to use document.body. 99 percent of the time, that's what you want. The container will actually be document.body, or it'll be a new div that has just been appended to document.body.
[31:37] Then we'll keep track of all of the containers that we're mounting to the document. We add that to our set of mounted containers. That's what we're going to use to later unmount all the containers when we clean everything up.
[31:51] You can render as many times as you want to in every test. Once you call react-testing-library's cleanup function, it'll clean up everything that was mounted, unmount it, and remove it from the dom so that every test can be isolated from one another.
Joel: [32:07] Is it set, like an array?
Kent: [32:09] Yeah, it's basically like an array that keeps unique values. You can't add the same value to a set twice, which is why we're using this here. You could call render with the same container twice in a row, and you do when you call rerender. Yeah, that's what mounted containers is, a set so that we don't add the same container twice.
Joel: [32:32] Makes sense.
Kent: [32:35] Then we have this wrap UI if you need it. If you provide a wrapper component, then we're going to wrap that for you with React.createElement, then pass the inner element. 99 percent of the time, you're not going to be doing this, either. Most of the time, we're just going to take the UI that U passes here and we're going to render it with ReactDOM.render.
[33:03] Yeah, go ahead, Joel.
Joel: [33:05] Quick question. You're rendering into the div as a container, right? Why wouldn't we just render it directly into the body? Is there a limitation or a reason for that?
Kent: [33:14] Yeah, that might be just historical reasons. If you try to render a React...at least, this was back in at least React 15, would have this warning when you tried to render it to document.body. It would tell you, "Hey, that's a bad idea." I can't remember exactly why that was problematic, but they recommended against doing that, so we do the same thing.
Joel: [33:42] Yeah, because reasons.
Kent: [33:44] Yeah, because reasons, yeah. Because Dan told me not to.
Joel: [33:48] That's a good reason, actually.
Kent: [33:52] Yeah. We're doing this inside of an act. I'm going to explain what act is later. That's not super crucial for you to know right now. For all intents and purposes, we're just doing ReactDOM.render or hydrate, if you are testing server-side rendering rehydration, something.
[34:10] Most of the time, people are doing render. We just render the UI, whether it's wrapped or not, then we render that to the container, which most of the time will be the div that we created ourselves.
[34:24] Pretty similar to this idea, fundamentally, that's all that code is. Fundamentally, we create a div. We append it to the body. Then we use ReactDOM.render to render that UI to that div.
[34:39] After that, we're going to return some utilities and other objects that could be useful for you. We return the container and the base element. Those are just in case. I'll talk about the distinction between these two really quick.
[34:55] The container is where your UI has been rendered to. That's going to be the div. You can specify your own container if you want to. Most of the time, people don't do that, so we created a brand new div. We're going to render things to that div.
[35:12] The base element is a little different. By default, the base element is going to be document.body. The base element is where our queries are going to be based off of. Here in our test earlier, we said document.body querySelector. We could also have said div.querySelector. That would work just as well. That passes our test just fine.
[35:37] The reason that we use document.body instead is for React portals. Most of the time, your document.body should only have your component rendered into it anyway. It doesn't really make much of a difference.
[35:55] If you're using React portals, you're going to be rendering something outside of this div somewhere else in the body. You'd have to do something special so that you could go get whatever you're rendering in that portal.
Joel: [36:07] Portals are for iframes?
Kent: [36:10] Yeah, iframes, modals are a huge case for portals. I want to render something within my React tree right here, but I actually want that to appear in the dom somewhere else. That's the idea behind portals.
[36:25] By making all of our queries based off of document.body, you don't have to do anything special when you start testing portals. That's the idea behind that. You can, of course, customize what the base element should be. Most of the time, using document.body works just fine.
[36:47] That's container and element. We also return debug. What debug does is, it accepts an element if you don't provide one. Then we'll just use the base element. It's going to console.log this pretty-dom function. What that does...Actually, you know what I should do? I'm going to show you that same test with react-testing-library.
[37:10] We'll just refactor this. That will, hopefully, help things a little bit. We're going to render. We don't need to provide a div. We're going to get, getByText is what I'll use here. We'll say, getByText zero. Here, we'll just do the string.
Joel: [37:36] Showing off with the regex.
Kent: [37:38] Yeah, man! Well, I'll explain why regex is useful later. I'm also going to pull in fire events.
Joel: [37:44] It's all Livestream, Kent.
Kent: [37:48] Nobody wants to watch regex live streamed.
Joel: [37:50] That stuff's fun.
Kent: [37:52] Oh, boy. It takes a special person. You're a special person, Joel.
Joel: [37:56] That's what my mom says.
Kent: [37:59] OK, cool. We've got two tests that basically do the same thing, but using react-testing-library now. One other thing that I'm going to do here is pull in cleanup. I'll show you what cleanup does. We'll say, after each cleanup. That'll just clean up my dom at the end of every test.
Joel: [38:25] That handles that teardown comment, then, right?
Kent: [38:28] Yeah, exactly, thank you. Cool. Actually, you know what's interesting? Before I added that cleanup, you'll notice that my tests are passing. Both of my tests are passing. Check this out. If I move this test to go first, that test is still passing. Wait a second.
Joel: [38:53] You might change the names.
Kent: [38:56] Yeah. No, I was expecting this to fail. It should be failing. I'm not going to look into why that's not failing. It should be failing, but I'm probably missing something. Anyway, getting back to the things that matter. Here we have this test. I was showing you a debug. Let me show you what debug does.
[39:18] Earlier, I was using console.log document.body, enter HTML. That was cool, but debug is much cooler. Debug will log out. Here, let me comment this thing out, because it's bugging me out that I still got that dom messed out, that teardown. I should have written that out.
[39:40] Debug is going to give me a formatted version of my dom, of document.body. It's more useful when you have a pretty sizable amount of dom that you're dealing with. It's pretty rock solid awesome.
[39:59] It also will format things like, "Hello." It color codes it, formats it really nicely. This is actually using the same packages that are used to serialize a dom for snapshots. Everything is formatted really nicely. Debug is a super helpful utility provided mostly by dom-testing-library. That's where the logic lives. All that debug does is it'll console.log pretty-dom, which is pretty cool.
[40:32] Joel, did you have a question about that?
Joel: [40:35] I wanted to ask, can you use debug to generate snapshots?
Kent: [40:41] Yeah, if you want to do snapshots. I don't actually recommend snapshots most of the time. In fact, I have a blog post about this also, snapshots. It's called, Effective Snapshot Testing. Basically, the idea is, don't use snapshots if you can help it.
[41:00] If you do need to do snapshots, then you're in luck. You can just pull out the container, or let's say we just wanted to snapshot the button. We could do expect button to match snapshot. Here, we'll do an inline snapshot. Those are not so bad. We save that, and our code will get updated with that snapshot.
[41:21] You can just pass dom nodes directly to expect, and it will take a snapshot of the dom. You don't have to do anything special. It just works.
Joel: [41:32] Awesome, thanks.
Kent: [41:33] Yeah. Cool, cool. OK, that's what debug does. It's awesome. It is purely for development time purposes. If you want to use the underlying API, that's pretty-dom. That comes from dom-testing-library. Maybe we'll look at what that does if we have a little bit of time.
[41:55] Cool. Oh, and also, with debug, I should mention, is a debug button. You can have it just log out a specific element as well. If you have a big dom that you're dealing with and you just want to focus on one thing that you're debugging, you can just pass that. Pretty handy.
[42:18] OK, we have unmount. Sometimes you want to test a component will unmount or your cleanup function for your effects. Make sure that you're cleaning up your set interval or whatever you're doing. Unmount is available for you. We'll do that right here.
[42:37] Here, we'll have to pull that out of here. It'll say, unmount. Then we can debug. Here, we'll do a debug before and after. You'll see before, the body is full. After, the body has an empty div.
[42:53] It doesn't remove our container from the parent. Our cleanup function does. This unmount will just unmount the component where it's at. Then you can test, make assertions on subscriptions being cleaned up or whatever. That is unmount.
[43:11] Rerender is nice. Let's say that this counter, if we were to make up some new prop that says, "message, hello." Then let's say that you wanted to rerender this with a different message and assert that the message did, in fact, change. I don't know. It's a contrived example, but hopefully it makes sense.
[43:40] Then you can pull out rerender. In here, you'd say, rerender. You will give it the new React element that you want to have. Lots of people see rerender as synonymous to setProps, because it's used to test the same kind of thing that you would use with setProps from Enzyme. It's a little bit different.
[44:03] Instead of allowing you to set the props on the component that you rendered, it allows you to render just about anything. You just pass it a new React UI element and it will rerender that.
Joel: [44:22] That's a whole new element, too, so you're not setting props. You're creating an entirely new element with different props.
Kent: [44:28] Yes, exactly. One thing that I should bring up that's a distinction there is that you are creating a brand new React element. This is a new React.createElement call. That's what happens every single time render is called inside of your component or your component function. Every single time, those are brand new elements.
[44:53] The difference, or the important distinction, is that you're not actually creating new component instances. Unless you were to provide a different key, which you could do if you wanted to, as long as the type that you are rendering is the same as what was rendered in that container before, then React is going to reuse that instance.
[45:18] That's why this is a rerender and not a render new, or render a brand new instance of my component. If you wanted it to be a new instance, then you could say, "Key is just a new key."
[45:32] Then React will say, "Oh, OK, this is a new thing. I'm going to create a new instance, unmount the old one, mount this new one." That's not a typical scenario for people. That is rerender.
[45:45] AsFragments, I don't honestly use this a whole lot, but this is useful for if you want to take snapshots. Sometimes, some snapshot serializers muck up the thing that you're snapshotting really bad.
[46:01] What asFragment does is it allows you to create, basically, a copy of the element that you have. Then you can pass that to your snapshot serializers. It can muck up the fragment, but it doesn't muck up your original container. That's all that really does.
Joel: [46:25] We got a question about rerender. If you want to test shouldComponentUpdate type behavior, would that be related?
Kent: [46:32] Yeah, that would be a perfect place to do rerender. If you wanted to say, hey, I'm using shouldComponentUpdate, and if the message is the same, then it should not rerender, you can say rerender with the same message. Then assert that whatever didn't happen.
[46:52] If you have shouldComponentUpdate and a component did update, you're firing a new fetch request or something, you just rerender with the same thing, then make sure that that fetch request didn't happen, or whatever you want to avoid in a rerender. Those kinds of things can be a little bit tricky to test. Often, unless it's complex logic in there, I won't even bother testing that.
Joel: [47:20] In a Hooks world, do we use shouldComponentUpdate anymore?
Kent: [47:24] We use React.memo. That allows us to do some of that. Typically, to be perfectly honest, in the three or four years that I've been using React, I have never used React.memo, shouldComponentUpdate, or PureComponent, ever, not once. Never needed to. Andrew Clark told me that I should default to those things. I just haven't because I've never really needed them.
Joel: [47:54] Default to not using them?
Kent: [47:56] He said I should default to using them.
Joel: [47:59] You were just like, "Whatever, man, I'm not going to do what you tell me."
Kent: [48:02] That's basically what I said.
Joel: [48:04] Nice.
Kent: [48:05] I'm my own man. Cool. That's Fragment. The way this works, I didn't actually write this. Apparently, there is a createRange thing that you can use in the browser. jsdom doesn't support it, but you can createContextualFragment, which basically makes a new element based off of the element that you provided. It's kind of interesting.
[48:36] Then the last thing, and this is the most interesting that we'll get into in the last few minutes, is the queries-for-element. Get-queries-for-element comes from dom-testing-library up here. That gives us all of the really nice queries that allow us to use our component the way that our end users are going to use it.
[48:58] Rather than looking for the first input, our users are looking for the input that has the label of username. They don't care what order things are coming in. They're going to be entering data based off of the labels. That's just one example of the many that get-queries-for-element allows you to have.
[49:14] We'll look into that. Let me just finish up this really quick. Cleanup, just for every one of our mounted containers, we're going to cleanup at the container, which basically says, we'll remove child container. We remove that from the body and unmount that component at that container. Then we remove that from our mounted containers.
[49:36] You just use this after each, kind of like this. React-testing-library has a couple of...a useful thing you can import that'll basically do that. You just import react-testing-library/cleanup-after-each. Just get rid of that and get rid of that. That's what it'll do.
[49:57] That's pretty handy. FireEvent, it's using dom-testing-library's fireEvent, but it wraps things in the act from React's test utils. That's all that's pretty much doing. We also do a couple of extra wonky things to make React events work. React does some weird things to make things like select work.
Joel: [50:23] This is to deal with React's SyntheticEvent system?
Kent: [50:26] Yeah. React does some weird things to make the SyntheticEvent system work properly. We've got mouseEnter and mouseLeave. Select is doing something special. Yeah, that's what those are for.
[50:40] Then we just re-export everything from dom-testing-library, except we override fireEvent. That is react-testing-library.
[50:51] We've just got a couple of minutes left. I want to show, not in any great detail, I want to show fireEvent, what that's doing, and all of the queries from dom-testing-library.
Joel: [51:02] Is dom-testing-library an order of magnitude more complex than this?
Kent: [51:07] I wouldn't say an order of magnitude more complex. It is definitely bigger. It's quite a bit bigger, but it's still relatively simple. A lot of code does not necessarily mean really complex.
Joel: [51:24] That's true.
Kent: [51:24] For example, events. This is a lot of code. It's over 350 lines of code, but it's not very complex because most of it...
Joel: [51:33] It's not complicated. It just has a lot of surface area.
Kent: [51:35] Exactly.
Joel: [51:36] Just the nature of the DOM, right?
Kent: [51:37] Yup, exactly. You have all of your events that have different event types. If you open up your browser dev tools right now and say, "window.," you'll be able to get a keyboard event, a focus event, input event, a regular event, a mouse event, all these different types of events that dom-testing-library supports.
[51:59] Then you can use fireEvent from dom-testing-library. By the way, this is the dom-testing-library repo we're in now. I should have mentioned that. You'd say fireEvent.mouseEnter or fireEvent.select or fireEvent.scroll. All of these things are supported. Dom-testing-library will dispatch those for you.
[52:21] There are a couple of things in here that do get a little bit complex. Here, we're going to take all of those events from that giant object. Actually, really quick, this little function is the pretty simple meat.
[52:36] This is the last line of code that's run before the event gets handed off to the browser, where we say, "fireEvent on this element. Here's the event we want to dispatch." All it does is it dispatches that event.
[52:49] Then we create a bunch of properties on the fireEvent, one for every one of these in that giant object that we had earlier. What these are going to do is it'll take a node. We're going to create a function that'll take a node and some initialization for that event. I'm going to skip over some of this stuff because it's specific things for different types of events.
[53:17] Fundamentally, we're going to get window. We'll get that event constructor. We'll construct to that event based off of that event name, for example, transitionEvent. Actually, sorry, no. This is the event name, transitionEnd. The transitionEvent is the event constructor.
[53:42] Then we'll take that event initialization, whatever you pass to us, plus the default initialization, which in our case is bubbles true and cancelable true. That will get us an event object. Then we'll use that fireEvent on the node with that event object. That makes things happen.
[54:04] There are some weird things we have to do, even in dom-testing-library, that are a little bit specific to React, that don't harm other implementations. That's the basic idea of fireEvent. It's pretty simple.
[54:20] The real lines of code or the most complex stuff is going on right here. Everything else is actually pretty straightforward. All this configuration and everything saves you a lot of headache. For example, if we look for -- let's see -- mouse. Right here. The click event, we default to button zero because that's actually what the browser does.
[54:52] If you were to dispatch a mouse event called click and you tried to click on a React Router link, for example, they have logic in the React Router link code to make sure that the button is zero, which is a left-click, so you're actually left-clicking on the link and not right-clicking on it or something else.
[55:14] That's a default in the browser when a user is clicking, but it's not a default when you try to construct the mouse event yourself or when you do a .click on a link. Dom-testing-library is covering some of those weird situations for you. It's pretty sweet.
[55:34] The last thing that I'll show you, and then we'll wrap things up here -- man, we won't even get to the waitFor utilities, which are really sweet, kind of a bummer -- is the queries. The queries are what allow you to interact with your component in a way that the user would. We have a whole bunch of these.
[55:57] I'll just show you examples here. You have getByText. That's a query. You also have getByLabelText or getByAltText or getByPlaceholderText. Anything that the user would see or be able to interact with via a screen reader, like accessibility tools. We also have getByDisplayValue for selects or inputs.
[56:26] Each one of these various queries has multiple forms. You have a getByText. You getAllByText. If it can't find something by that text, for example, if I were to say we're getting by text five, then it's going to throw an error. It's going to log out what the DOM looks like. It'll log out this message to explain what's going on.
[56:54] That's great unless you want it to verify that something does not exist in the DOM, that something's been removed or something. There's also a queryBy, queryByText, where if it can't find it, then it'll result in null. Button is null. That is why we're getting this error.
[57:15] For each one of those, you also have a all variant. You have getAll and queryAll. If you had multiple of these or multiple buttons, that's going to return an array of those things. For the get, if that array is empty, then it's going to throw an error again, like we had before.
[57:44] That's the general idea. We're also probably going to add a find variant, where you could say findAllByText. This would be an awaitable or a promise-returning function that would keep on waiting for the document to be updated for that query to work out, which is, I think, going to be a pretty cool API.
Joel: [58:09] We didn't get into async-type stuff. That's a whole new...
Joel: [58:14] A lot of this is async by nature, I think. It's hidden from us, like if you have long-running async stuff going.
Kent: [58:25] Actually, async is where dom-testing-library and react-testing-library really shine because it has some really nice utilities for you. Maybe I'll just show off a couple of those really quick. First, I'm going to...
Joel: [58:39] That's the waitFor stuff you were saying it's a bummer we couldn't talk...
Kent: [58:43] Exactly. Really quick, I'll go through these queries. Each one of these is going to be a little bit unique. Probably the easiest one is queryAllByText. This is probably the most relatable as well.
[58:59] Each one of the queries, the fundamental where the logic lives is in the queryAll variant because everything else can be built on top of the queryAll. This is going to take a container and text. You'll notice that here we're not actually accepting a container. We're just accepting what's called the text match, so the text.
[59:22] That is because react-testing-library is using pre-bound versions of all of these queries where the container is provided upfront. You only have to provide the rest of the arguments. Because we know what the container is if you're rendering, we know what that container is going to be. getByText, you don't have to provide the container.
[59:45] Here, we have queryAllByText. We take a container and the text. We have some options that you can specify. Let's see. We normalize your matcher, which basically...I'm not going to get too far into it. We'll talk more fundamentals here.
[60:07] We're going to take that container. We're going to querySelectorAll based off of that selector. Default is star. Basically, take the container. Get all of its descendants. If you're doing document.body and you've got a big dom, this is going to get everything that's in the document and turn that into an array.
[60:26] We're going to filter those by the ignore that you provide. By default, we'll ignore scripts and styles because you never really want to make assertions on those. That'd be weird. We'll filter those by whether your matcher matches your text, in our case.
[60:46] It could be a regex. It can be a function. It can be a string, whether or not it matches and then we return the remaining array. The different variations of the byText query will respond to that differently.
[61:01] The queryByText is going to take the first element of that and return that. The getByText is going to throw an error if there are more than one or less than one. If it's not only one result, then it's going to throw an error, or it'll just return that one. getAllByText will throw an error if it's empty or will return that full array.
[61:30] Find all and those different things, are going to behave appropriately for those as well.
Joel: [61:35] This is the really helpful stuff, in terms of getting our components and honing in on what it is we want to test, and finding the elements.
Kent: [61:44] Exactly.
Joel: [61:44] In general.
Kent: [61:45] Think about it as a manual tester or an end user. We want to make sure that when the end user uses our application, they're going to be able to do it successfully. The end user does not look for the submit button. They look for the button that says submit. We're going to do the same thing in our test. We're going to look for the button that says submit.
[62:28] That's what all of these queries are enabling you to do. This is the fundamental way that those work. Some of these, like the query all by label text, this one gets a little bit more complicated. What you have to do is you find the labels, and then you find the form control that's associated with those labels.
[62:55] It's pretty easy in the browser because the browser is going to give you .control property on the label node. In jsdom, it doesn't do that. You have to get the for attribute and return that. If there's not a for attribute, then there's an ID, and then you'll look for the all your label by, and a bunch of other things. It can get a little bit more complicated, but we've done it all already. It's working great.
Joel: [63:19] That's the trouble now. We don't have to worry about that too much, I don't think.
Kent: [63:25] Exactly.
Joel: [63:26] Kent, I think we're running over anyway. We'll probably wrap it up. Maybe we'll have a fun wait for DOM change async overview at some other point. What do you think?
Kent: [63:36] Yeah. That sounds cool. I'll just really quick...we've got wait for element. It's using a MutationObserver. If you go to DOM Tips with Kent on YouTube, I show how to use a MutationObserver. That's how this thing works. It's actually really cool. Wait for DOM change does the same thing.
[63:54] Wait itself is actually super simple. All that it does is it uses wait for expect, which you can go look into, and it's just waits. Every 15 milliseconds, it'll check your callback, and it'll wait until that stops throwing an error. That's all that does. Pretty simple. Now, we don't need to do that. Hopefully, that was helpful to everybody watching.
Joel: [64:16] It was helpful to me. One thing I think that's cool about this is now, I can go explore these libraries myself and have a pretty good idea, in terms of what's going on, and then just using them as a great overview. I really appreciate it, Kent. Thanks for taking the time to show us and give us this tour of these cool libraries that you've put out into the world.
Kent: [64:39] Thanks. I appreciate the time. I'm glad it was helpful.
Sure, here you go https://reactjs.org/docs/test-utils.html#act
Amazing! Thanks both Kent and Joel!
Hey, great talk! really helpfull, but sad you forgot to talk about act() function. I'm not sad really. ¿Could you please give some clue about what it does? Thanks!