React Testing

Learning Goals

By the end of this lesson, students should:

  • Be familiar with the Jest/Enzyme syntax
  • Be able to test the rendered output, state changes, and event handlers in your React components

Review: Four Phases of a Test

Turn and Talk

What are the four phases of a test? What type of logic do you write in each phase of a test? (Think back to your GameTime projects - what are some specific examples of code you wrote in each phase?)

(All of these same phases and concepts are going to apply when testing our React applications!)


Jest & Enzyme

So far, you’ve seen and used Mocha & Chai for testing purposes. Now that we’re moving into React, we’re going to use some slightly different tooling for testing our applications. The concepts, patterns and fundamentals of testing have not changed, just the tools and syntax we’ll be using to do it!

Jest

  • Automatically finds tests in your codebase (very similar to mocha!)
  • Automatically mocks React dependencies
  • Runs your tests with a fake DOM implementation (think about how we didn’t test our DOM manipulations in GameTime because the terminal did not know what a document or window was)

Enzyme

You can think of Enzyme as jQuery for Jest – it makes our test files a bit easier to read and write, and provides us with a lot of helper methods for testing common pieces of functionality.


What Are We Testing

We’re going to test three main things in each of our React components:

  • What gets rendered: given certain props, does the component ultimately render the HTML that we’re expecting?
  • State changes: does the state of our component change in an appropriate manner as we call certain methods?
  • Event handlers/actions: do our buttons/forms/etc. respond to user interaction in the way we’re expecting?

In order to effectively test every component, we must make sure our methods strictly follow the single responsibility principle. The smaller our methods, the easier they are to test. We’ll use the Trivia App repo we’ve been working with to practice some examples of each type of test we’re looking to write.


Setup

As you may have expected, we need to install some dependencies from NPM before we can get started with testing our React components. create-react-app by default comes with Jest installed, but not Enzyme. We’ll need to install Enzyme and set up an adapter to let React know that we’re using an additional tool when we run our tests:

npm install --save-dev enzyme enzyme-adapter-react-16 enzyme-to-json

Next we need to update our package.json files to configure some of our test output (more on this later):

  "jest": {
      "snapshotSerializers": ["enzyme-to-json/serializer"]
    }

Lastly, we need to tell React to use the Enzyme adapter we just installed. In your /src directory, alongside all of your components and their test files, create a new file called setupTests.js.

This setupTests.js file can be thought of as a configuration file for React testing. This time around, all we’re using it for is to tell React we’ll be leveraging Enzyme. You will learn additional use-cases for this file as you get into more advanced testing, but for now, just keep in mind that it is allowing us to configure our test environment.

Add the following code to your setupTests.js file:

import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });

STOP! Double Check: is your file named exactly setupTests.js? (This file name is case-sensitive, so capitalization counts!) is your file in the /src directory? If not, you will not be able to test any of your files.


Snapshot Testing

The first type of test we’ll tackle is a Snapshot Test. This is what allows us to test the rendered output of our component. Open up the TriviaList.test.js file:

What dependencies do we need to import?

We’ll need React, Enzyme, and the component we’re actually trying to test:

import React from 'react';
import TriviaList from './TriviaList';
import { shallow } from 'enzyme';

From Enzyme, we are only importing a single helper function called shallow. This function will help us create a basic representation of the rendered output for our component, called a shallow render.

What kind of mock data might our TriviaList component need in order to render?

(Hint: Look at where you’re actually rendering TriviaList in your component files. What data are you passing through to TriviaList?)

If we look at our App.js component where we’re actually rendering our <TriviaList>, we can see that it’s expecting to get two props: triviaQuestions and questionCount. Let’s create those both as variables in our test file so we can include them when we render our component:

const mockQuestions = [{category:"Entertainment: Video Games",type:"multiple",difficulty:"hard",title:"When was the first Call of Duty title released?",correct_answer:"October 29, 2003",incorrect_answers:["December 1, 2003","November 14, 2002","July 18, 2004"],id:1},{category:"Entertainment: Books",type:"multiple",difficulty:"medium",title:"How many books are in the Chronicles of Narnia series?",correct_answer:"7",incorrect_answers:["6","8","5"],id:2},{category:"Entertainment: Film",type:"boolean",difficulty:"easy",title:"Han Solo's co-pilot and best friend, 'Chewbacca', is an Ewok.",correct_answer:"False",incorrect_answers:["True"],id:3}];

const mockQuestionCount = 2;

Note: A good naming convention for your mock data is to keep the original variable name the same, so you can easily recognize what it represents, but prefix it with the word mock.)

What kind of setup do we need to do?

We’ll need to set up our component so that we have something to write our expectations against. This is where that handy shallow function will come into play.

Remember we always write our set up code in a beforeEach block, to reduce the amount of duplication in our it blocks:

describe('TriviaList', () => {
  let wrapper;

  beforeEach(() => {
    wrapper = shallow(
      <TriviaList
        triviaQuestions={mockQuestions}
        questionCount={mockQuestionCount}
      />
    );
  });
});

The shallow render will create an Enzyme ‘wrapper’ for us around an instance of our component. Now, in each of our tests, we’ll be able to interact with this wrapper variable as if it were our TriviaList component.

Writing the Snapshot

Finally, after all this set up, we’re ready to write our snapshot assertion:

it('should match the snapshot with all data passed in', () => {
  expect(wrapper).toMatchSnapshot();
});

This expectation will tell our testing tools to generate a file that includes a representation of our rendered output. If we head over to our terminal and run our tests with npm test - we should see that we have a passing App test.

You can also check out the snaphshot file that was generated for us by writing this expectation. In your /src directory, you should now see a __snapshots__ folder that includes a TriviaList.test.js.snap file. Take a moment to look over the contents of that file.

On Your Own

Write a snapshot test for the `Question` component. Ask yourself the same questions we just went over: what do we need to import, what mock data might we need, what setup must we do? No copy pasting the code we just wrote! You will want to develop your muscle memory here by typing everything out from scratch.

As you get stuck, or as you finish, pair up with the person next to you to compare your logic.


Testing State

Now that we know how to test our rendered output, we can start testing some application changes like state updates. This Question component you just tested is a bit more involved than the TriviaList component from earlier - it has some state that can change in response to a user interaction.

Validating Default State

Let’s first write a test that validates the default state of our component.

In a new it block, we want to assert that our state value for showAnswer is initially false. We will make use of another handy method from Enzyme called state() in order to access the state of our component:

it('should have the proper default state', () => {
  expect(wrapper.state()).toEqual({ showAnswer: false });
});

Solo Research

Read the Enzyme documentation on wrapper.state(). Based on this documentation, what's an alternative way we could have written the above assertion?

Validating a State Change

Not only do we want to check that our default state looks OK, we want to make sure any methods that call state changes also behave as expected. In our Question component, we have a method called toggleState that flips the boolean value of showAnswer answer in our state. We want to make sure that any time we invoke this method, showAnswer has been updated.

There are several ways to test methods on a component. For this example, we will directly test the showAnswer method by invoking it.

In a new it block, we want to first assert the default value of showAnswer, then invoke our toggleAnswer method, then assert that the value of showAnswer has changed:

it('should update the answer state when toggleAnswer is called', () => {
  expect(wrapper.state('showAnswer')).toEqual(false);
  wrapper.instance().toggleAnswer();
  expect(wrapper.state('showAnswer')).toEqual(true);
});

Note: We cannot simply call wrapper.toggleAnswer() to invoke our method. The wrapper returned to us from our shallow render has a lot of Enzyme-related padding around it, so we need to dig into the actual instance of our component in order to access the methods we’ve defined on it.


Testing User Interactions

Now that we know how to test rendered output and state changes, the last piece we want to make sure we can validate is that our user interactions are hooked up properly. This means that any click, change, focus, blur, etc. events that should be triggering a method are doing just that!

Let’s take a look at the Controls.test.js file, where we have some user interaction to test. One event we’ll want to test is this click handler we specified to set the limit of our questions:

<button className="set-filter" onClick={this.handleClick}>FILTER QUESTIONS</button>

We’ll want to make sure that when a user clicks on that button, our handleClick method is invoked and does what it’s supposed to. If we take a look at the handleClick method, we can see it’s invoking a method called setLimit that was passed down as a prop.

Because this method was passed down as a prop, we can assume that the method exists in some other component (App.js). We only ever test the functionality of our methods where they exist. (e.g. we’ll test the functionality of setLimit in the App.js component). So for our Controls component, instead of testing the functionality of setLimit, we’ll simply test that it was called. Is this pattern familiar from testing GameTime? What can you liken this to?

Creating a Mock Function

We saw that we need to pass in setLimit as a prop to our Controls component. Because we don’t care about the functionality inside of setLimit, we’ll set up a mock function:

const mockSetLimit = jest.fn();

Mock functions created with jest.fn() allow us to assert against the fact that they’ve been called, without having to worry about what logic was actually inside of them. Now we can pass in mockSetLimit as a prop to our component, as we’ve been doing:

describe('Controls', () => {

  let wrapper;

  beforeEach(() => {
    wrapper = shallow(
      <Controls
        setLimit={setLimitMock}
      />
    );
  });

And we’ll be able to test that it was called by expecting setLimitMock.toBeCalled()

Finding Elements in the Component

The first step to testing a user interaction is finding the element we want to interact with. In our case, it’s a button with the class set-filter. We can find elements in a component with the following syntax:

wrapper.find('.set-filter')

Solo Research

What does this syntax remind you of? Explore the documentation on find() and take note of the many ways you can search for an element in your component.

The second step is to simulate an event on your element. We want to simulate a click event on our set-filter button.

Solo Research

Take a moment to read the documentation on the simulate method before beginning. What arguments does it take in? How would we need to write our simulation for our button click?

Try taking the following pseudocode and turning it into an actual expectation:

// it should invoke setLimit when button is clicked
// find the button by its class name of set-filter
// simulate a click event on that button
// expect that our mockSetLimit function was actually called

On Your Own

Test the user interaction that triggers the updateCount method. What event do we need to simulate? What information might we need to pass into our simulate method? What expectations should we be writing?


Additional Practice

Turn and Talk

The last component to be tested is our App component. With your table, discuss what tests need to be written for this component. Once you've decided on the tests to be written, with your partner, write them!


Further Reading

Lesson Search Results

Showing top 10 results