Transcript from the "Unit Testing a React Component" Lesson
>> Kent C. Dodds: Here, I'm going to first import React from 'react'. We're gonna be using JSX React.createElement, so we need that. We'll also import the Editor from '../editor'. And then, we need to arrange things however we want. So I'm gonna make a container div for rendering. So I'll have document.createElement ('div').
[00:00:31] And then I'm actually gonna be rendering, so I'll import reactDOM from 'react-dom'. And then, we'll do ReactDOM.render, my UI Editor, with the container. And let's actually take a look at what this thing really is expecting of us. There we go, it should actually be pulling in the to do
>> Kent C. Dodds: todo, okay? Okay, so the editor is pretty simple. It's a form, and then it has a submit handler that we'll be testing. So yeah, so we rendered that. We're gonna look for all of the form elements, fill them out, and then submit the form. So let's get our title, content, and tags.
[00:01:33] title = container, how are we gonna get this? So spoiler alert, we're gonna have a much better way to get this in a little bit. But I thought it would be good for you to feel the pain first. [LAUGH] So we've got the form. One thing that we could do is, all of these form elements have a name, and that's kind of a handy thing.
[00:01:58] So let's go ahead and get our form first. We'll say const form = container.querySelector('form'). And then, it will log the (Object.keys(form.elements)). So you can see with that looks like. To run these tests, and you'll be doing this as well, npm run. Or here, let me make sure. I think it's, yeah, npm run test:client.
[00:02:25] Make sure you only run the client side test. Okay, so you get zero, one two three. That's, hold on a second, that's not right. elements.title, yeah, there we go. I guess that's not one of the keys. But yeah, so the form, and this is not React stuff. The only React thing right here, is right there.
[00:02:49] That's the only React thing, all the rest of this stuff you could do with Vue, Angular, anything else. Because there you're just dealing with DOM nodes. And so this form in the DOM has elements and that has, actually, it's an HTML collection. And so you have zero, one, two, three that's associated with each element in the form.
[00:03:12] But then it also has properties for every named item in that form, it's pretty nifty. So we can get the title, I wonder if destructuring works with this? I haven't tried this. See if we can get the title, the content, and the tags. Title, yeah, cool, that's handy.
[00:03:35] And then with that, we can fill those things out. So I'll say title.value = 'foobar'. Let's do something more interesting, 'I like twix'. And the content value is also text area. So we'll just set the value of the text area, Like a lot Sorta, I guess. And then tags, so tags is an input.
[00:04:02] And it's expected to have comma-separated tags. So one of the features we might wanna test is this logic right here, where it will split by the commas and then trim those, so that they don't have spaces. So we'll do this, we'll say tags.value equal twix, my, likes. So we kinda get some of that testing there.
[00:04:25] And here we can even do a couple after, yeah, cool. So now that all of these form values are set, and as far as our test is concerned, or our source code is concerned, it doesn't actually matter how those values get set in there. This isn't a dynamic form, it's a static form.
[00:04:47] And so the form has values, and then the submit is fired. Now, if this was a dynamic form, it had onchanges, then we would have to do some extra stuff. And we can talk about that later. But yeah, for our purposes, we can just set the values as they are, not have to worry about firing onchange events or anything like that.
[00:05:07] But we do now need to fire a submit event on this form. And so that's what we're going to do. Say, submit = new Event('submit'). So it's like a window.Event. [COUGH] And then we'll say form.dispatchEvent(submit). Don't worry, this isn't how you're gonna be doing all of your tests.
[00:05:35] We've got some utilities that we'll use later. But I think it's really important for you to understand your abstractions, and this is how they work. Or at least it's similar. Okay, so with that, our handleSubmit should get fired. So let's, actually, I'm just gonna throw an Error('here'). And we'll test that, here it is.
[00:05:59] Yay, jazz hands. Okay, so now actually, I have this fancy thing I call throw log, that will let me stringify anything. And it looks like we're failing before, cannot read property id or undefined. Yep, we're missing a prop. So let's pass a fake user, that's fakeUser here. User is our fakeUser.
[00:06:31] And it looks like that fake user just needs an id for the author id. We're creating the codes for that, so id: "foobar", cool. And then that's throw log. So this is what things look like. We have this newPost, all of that stuff. That's great, okay. So if I remove that throw log, what's gonna happen is this api.posts is gonna get called.
[00:06:58] And we're gonna have a bit of a problem.
>> Kent C. Dodds: What do I want to explain? Suffice it to say that we don't actually want to make an HTTP request when we're writing in a test. That would make our test very brittle, very slow. And there are other kinds of tests that we can write to cover that.
[00:07:25] We'll talk about that with the end to end test. For right now, let's just make sure that API is being called properly. So there are couple of ways that we can do this. And we talked about some of those in great depth in my workshop yesterday. And actually, where'd the slides go?
[00:07:45] Here they are, those are all messed up. But here is another blog post where, if you want to get in deeper on the subject we're about to talk about, it's mocks. And so I'll take you through working your way to a good mock solution with Jest. So go check that out if you're not super comfortable with what we're about to do.
[00:08:08] But we need to mock this API module. So yeah, let's go ahead and do that. We'll say jest.mock, and we need a relative path, that's relative to this module where we are right now. So it will be up into the tests, up to the screens, so on and so forth.
[00:08:32] And so that is going to be one directory more than this one.
>> Kent C. Dodds: And we want to mock that with our own custom mock here that will return an object, which is our mock object. So when this pulls in API, instead of the actual API module, it will pull in whatever we're returning from here.
[00:08:54] And so we'll have posts is something on the mock object. And that's an object that has a create method. And we'll make that a jest function. So that's just a fake function, it keeps track of how it's called, when it's called, that kind of thing, and what it's called with.
[00:09:13] And we need it to return a promise. That promise doesn't need to resolve to anything, so we'll just have it return to Promise.resolve. So a resolve promise, it'll happen instantly or on the next tick of the event loop. Okay, cool, good. So, cannot read push of undefined, looks like we also need the history prop.
[00:09:36] So this is history from React Router, and because this is a React Router screen. And so we'll just make a fake history. const fakeHistory equals, something that resembles this same API. We could actually just make it an array if we want. But I don't want to do that.
[00:09:58] We'll just make a, well, yeah, we'll make it a push method that is a jest function. And then we can make assertions on that. Okay, then we say history is our fakeHistory. You do this a lot when you're unit testing, making fake versions of things. Especially if it's coming in as a prop, you'll do that.
[00:10:26] If it's a module dependency, you'll use this jest mock thing. In a second, I can show you how we could actually pass this as a prop as well, using kind of a pseudo-dependency injection thing. So yeah, now our tests are passing. But we're not making any assertions, we're just, the code runs.
[00:10:47] So let's go ahead and make some assertions. So we've got a couple of functions. Probably the easiest assertion we can make right now is expect(fakeHistory.push).toHaveBeenCalledTimes(1. So that should pass. But it doesn't, what? No, no, this is good. So the reason that fails is because this is asynchronous. Okay, so we need to wait for that to resolve.
[00:11:25] And so I've got this handy little trick that I call flushPromises. And that is just an arrow function that uses setTimeout.
>> Kent C. Dodds: Well, actually, it's kind of more complicated than that. It'll be return a new Promise that resolves,
>> Kent C. Dodds: And inside of here, we'll do setTimeout with resolve, and then 0.
[00:11:58] So what this does is it means that this thing will wait until the next tick of the event loop before it is resolved. And if we make this an async test, then we can say await, till the next tick of the event loop. And then we'll run the rest of the assertions, cool.
>> Kent C. Dodds: Again, we've got utilities for all this stuff. And your tests will actually be a lot simpler than this. And so this is mostly to give you exposure to what's possible, and then you can look at it later. So we do wanna make a couple of more assertions.
[00:12:38] And those will be toHaveBeenCalledWith. So it was called but was it, are we actually redirecting the user to the homepage? We'll assert that and make sure this is running, not. This is actually one case where you really wanna make sure that your assertions are running, because we're dealing with asynchronous stuff.
[00:12:57] And so maybe this never resolves, but the test still finishes or something like that, and so these assertions never run. So you really wanna make sure that they run, and that they're testing what you think they are. So I can make it fail by changing my source code.
[00:13:11] Because this fails, I know that I'm testing what I think I am. Cool, so what else can we test? We can make sure that api.posts.create was called with the newPost. So, we'll expect api, or fake, sorry, sorry. Where did you go? We need to get access to our fake mock.
[00:13:35] And a handy thing with this is we can actually import. So, as utils, from the same path utils/api. And this will actually make it so, even within the same file, we're importing that mock that we create here. So this, we can call it utilsMock.
>> Kent C. Dodds: Okay, so we'll expect the utilsMock.post.create toHaveBeenCalledTimes(1).
[00:14:07] I think this is actually supposed to be posts. Thank you, ESLint, sometimes you are helpful. That happened too fast. Let's let's make sure, okay, cool. So it was called once, and now we wanna make sure it was called with the right stuff.
>> Kent C. Dodds: We then call it with just an empty object.
[00:14:25] See, great. So it's gonna have at least, the authorID will be the same as our fakeUser.id. And the title will be like our title.value. And the contents, content.value. And then the tags, there are couple of things that we could do with this. We could actually rewrite the logic in our test, probably not a good idea.
[00:14:53] Cuz what if it's broken in both places? So what we're gonna do instead is, we'll say twix, my, and likes. And the reason that it's broken is because the date, this one's tricky. So we could say date is expects.any(String). It's what it looks like, and then just be cool with that.
[00:15:25] I'd probably actually be cool with that, but I'll just show you this solution really quick, an alternative way to verify that the date is what we think it is. Don't look at anything else, that's cheating. But here, right before I submit, I create a predate. And then after this submit is finished, then I create a postdate.
[00:15:45] And then I make sure that the date that is called with is greater than or equal to the predate, and less than or equal to the postdate. So they created that date, the post was created between before I started creating it and after I created it. So if you really want to get deep into verifying this, then you could do things that way.
[00:16:08] I think I'm gonna be cool with just this, for sure.