Consuming a Context in a component can make it tricky to test, because it requires an invisible Provider outside the scope of the component being tested. In this lesson you’ll learn how to test a component that uses a Context Consumer.
Instructor: [00:00] This messageViewer component uses the email consumer. It expect to be rendered inside an email context. Let's see how we can test this. First, we'll yarn add react-testing-library. Then we can run yarn test to start up our test watcher.
[00:15] Then our project will create an tests directory underneath source. Inside there, we'll create a messageViewer.test.js file. In here, we'll import react, because we're going to be using JSX, and import render and fireEvent from react-testing-library.
[00:38] We'll also want to import emailContext from email-context. This'll be the full context object, not just the provider. Then we'll import messageViewer, which is the component we're testing. Let's write a test for when we're viewing this email.
[00:52] We should see the subject in the header, and this body in the div. We'll write a test, view an email, then we'll call render, and render out an emailContext.provider, with a messageViewer inside. Into this provider, we'll pass a value prop, setting currentEmail to some fake email, which we'll create up top.
[01:13] We have to give it a subject and a body. We'll destructure this render call to pull out the container. Then we can write our assertions. We can expect the container.querySelector on the h2 and the textContent from that. This should equal email.subject.
[01:32] Save this, and we get an error, because the path to messageViewer's wrong. Fix that, and now, our test is passing. Then we can write another assertion for the body. We'll expect container.querySelector h2 plus div.
[01:46] The div after the h2, and the textContent in there to equal email.body. When we save, the test still passes. Now, let's add another test for the back button. I'll say test backButton, and we're going to need a mock callback function here, which we can create with jest.fn.
[02:05] Then I'll copy and paste our render from the test above, because this one's going to be really similar. We'll just add a second property to this object, which is onSelectEmail, and pass in our mock callback. Now, we can click the button with fireEvent.click, and pass in container.querySelector button.
[02:27] After that, we can expect for mock callback to be called with null. Now, we can save, and this test works, too.
I wonder if this approach is testing the right thing. Why not provide the props that <MessageViewer /> needs to render, instead of basically testing that Context itself is passing those props? This sounds like the Redux equivalent of not testing Connect(), just testing the isolated component.
Viktor - I like the idea of extracting the rendering into beforeEach, but in this case the second test is passing a different value to the Provider (with the addition of the mockCallback). Maybe I could've extracted that out into a function. But yeah, I wanted to keep this focused on testing Context, not get into the weeds of refactoring tests anyway ;)
Andrew - I agree with you in general, but in this case, the component doesn't accept props. Since it has to get its values from Context, it has to be wrapped in a Provider. In the Redux case, usually you can export { TheComponent }
as well as export default connect()(TheComponent)
so you have a choice of which to test. In this case it's just the one MessageViewer
and it requires Context to work. If it were broken out into MessageViewer
(that took props) and ConnectedMessageViewer
(that used context), then I think your approach would make sense.
Note: The 'before' code for this lesson is the code from the previous lesson.
the back button test, for me it doesn't test anything at all, I felt it's not complete, because toBeCalledWith surely work, why would we event want to test it that way?
@Ming: The back button test mocks out the dependency (the onSelectEmail
function) in order to test just the MessageViewer
. It's a unit test, solely verifying that the MessageViewer's button is working properly -- that it calls its callback when clicked. But it draws the line there. It says, "I'm only testing MessageViewer; I don't care what happens after I call onSelectEmail."
You could instead (or additionally) write a test to verify that the screen actually changes when the button is clicked. That'd be more of an integration test, testing the combination of MessageViewer + EmailContext + MessageList, which is still useful and valid, but depends on more parts of the system and could make it harder to determine the root cause of a test failure.
@Dave, amazing, what's your trick to import different things so fast ?
@Dave, amazing, what's your trick to import different things so fast ? You seem to be going so fast, and I'm not sure how you do it, do you mind sharing ?
@Etenne-Joseph: Some of that is video editing, but a lot of it is from snippets I've set up in VSCode. A few of them are "ir" to import React from 'react'
, "i" to import a named thing, "id" to import a default from a file (where it assumes the variable will match the filename). So for example I can just type "i" <TAB> and then fill out the stuff in the braces, TAB again, and fill out the stuff in the quotes. Check out VSCode > Preferences > User Snippets to make your own.
Decent content course Dave👍. One small improvement I think would be just to reuse the rendered container with beforeEach way. Anaway this not an deal as the course is not about testing.