Test Component Boundaries

One common pattern that we have used in the word counter is lifting the state into a parent component and updating its children via props. A good way to test is to instantiate the parent component, trigger some events on it, and check whether the children update correctly.

For this type of test, it’s enough to render a component and its immediate children. Skipping the creation of the whole DOM structure will speed up the tests considerably. To perform this shallow rendering, we’ll use a library called Enzyme.[16] We’ll construct an element and its immediate children, then simulate the user interaction on the parent, and check that the changes propagate correctly to the children.

Start by installing Enzyme and its dependency as development dependencies:

 $ ​​npm​​ ​​i​​ ​​--save-dev​​ ​​enzyme​​ ​​react-test-renderer

The react-test-renderer library renders React elements to JavaScript objects without depending on the DOM, and Enzyme lets us more easily traverse these objects and make assertions about them.

Let’s now test the WordCounter component. We’ll create an Editor instance, simulate filling it with text, then check that the word count and progress bar update correctly.

Create a file in the __tests__ directory named WordCounter.test.js. To render WordCounter, we’ll use the shallow function from Enzyme, instead of ReactDOM.render. shallow only renders the component and its immediate children.

To use shallow, we’ll need to create React elements, so import React at the top of WordCounter.test.js. Also import the shallow function from Enzyme and the WordCounter, Editor, ProgressBar, and Counter components from your own project:

 import​ React from ​'react'​;
 import​ { shallow } from ​'enzyme'​;
 import​ WordCounter from ​'../WordCounter'​;
 import​ countWords from ​'../countWords'​;
 import​ Counter from ​'../Counter'​;
 import​ Editor from ​'../Editor'​;
 import​ ProgressBar from ​'../ProgressBar'​;

The Enzyme module exports multiple functions instead of a single default export. To select the import you need, surround the name of the function you wish to import with braces.

Similar to how we tested different arguments for count, we’ll create two separate tests: one for the counter and one for the progress bar. When we test user interfaces, it’s easy to get lost in details. Tests should assert something meaningful about the behavior of your application. We’ll start by testing the core functionality of our application: we’ll check that the counter displays the exact number of words when somebody types in some text. This way, a test failure will indicate that the application doesn’t work anymore.

The setup is identical for both tests, so we’ll house them both in the same suite and set up the WordCounter only once at the start of the suite. Create a new suite with describe and use shallow to render a WordCounter element directly inside the describe callback:

 describe(​'When I type some words'​, () => {
 const​ target = 10;
 const​ inputString = ​'One two three four'​;
 const​ wordCounter = shallow(<WordCounter targetWordCount={target} ​/>​​)​​;
 });

Store the target word count and the input string to be able to check the output later. Create a WordCounter element and pass it to shallow. Set the targetWordCount prop to the target constant you just declared. Jest automatically reads the .babelrc configuration and applies the required transforms to the test source, so you can use JSX in the test files.

shallow returns a wrapper that exposes some functions to traverse and manipulate the React elements. Now that we’ve created a shallow WordCounter element, we’ll simulate typing the input string with the simulate function that Enzyme defines on the wrapper. simulate calls the event handlers that you defined on the React component, but does not generate real events. It also works with the React event system, so to type some text, we need to use the change event instead of the native browser input event. Nevertheless, it’s a good enough substitute if we assume that React itself correctly translates events into calls to the vent handlers. First, retrieve the Editor element. To generate the change event, we then need to access the <textarea> element. But shallow only renders one level deep, so to access the <textarea> element, we need to render an additional level. We can do this by calling dive on the Editor wrapper. dive renders an additional level of elements.

 const​ wordCounter = shallow(<WordCounter targetWordCount={target} ​/>​​)​​;
»const​ textarea = wordCounter.find(Editor).dive().find(​'textarea'​);
» textarea.simulate(​'change'​, { target: { value: inputString } });
 });

We call find on the WordCounter wrapper and pass it the Editor component to retrieve all Editor child elements. We know there’s only one Editor, so we can assign the return value and use it without further checks. Since <textarea> is a base HTML element, there’s no corresponding React component, so retrieve the <textarea> element by passing ’textarea’ as a string to find.

simulate takes an event name as a string. It invokes the event handler corresponding to the event name, with the event data you pass as the second argument. Editor reads the text from the event.target.value property, so pass the text you want to input in the target.value key in the event data.

Once you have simulated typing in the Editor element, create a test to check that the word counter updated correctly. Retrieve the Counter element and check the value of its count prop against the value returned by countWords:

 it(​'displays the correct count as a number'​, () => {
 const​ counter = wordCounter.find(Counter);
  expect(counter.prop(​'count'​)).toBe(countWords(inputString));
 });

As before, we define the test with it. This time, we pass the Counter component itself to find to retrieve the Counter element. Another advantage of this method of finding child elements is that the test keeps working if we change the Counter to generate different markup. To check that the Counter element updated correctly, we call prop on the Counter wrapper to access its count prop. prop takes a string containing a prop name and returns the corresponding prop value. Showing how the expected value is calculated makes the intent of the test clearer. Calculate the expected value with countWords and compare it to the count prop value with toBe. If you run the tests again, Jest reports an additional passing test.

 PASS src/__tests__/count.test.js
  the counting function
  ✓ counts the correct number of words (2ms)
  ✓ counts an empty string
 
 Test Suites: 1 passed, 1 total
 Tests: 2 passed, 2 total
 Snapshots: 0 total
 Time: 0.066s, estimated 1s

Let’s check that the progress bar also updates as expected. Simulating the typing at the start of the WordCounter suite allows us to access the same wordCounter instance with the same state in all tests, so we won’t have to repeat the setup. Create a new test in the same suite where you retrieve the ProgressBar element and verify the value of the completion prop. Pass the ProgressBar component to find to retrieve the ProgressBar element, then check that the completion prop matches the expected value by calling toBe:

 it(​'displays the correct progress'​, () => {
 const​ progressBar = wordCounter.find(ProgressBar);
  expect(progressBar.prop(​'completion'​)).toBe(
  countWords(inputString) / target
  );
 });

Because of shallow rendering, you have to access the props instead of the values in the generated HTML.

To help pinpoint errors, test your application by working through shallow slices of your element tree, manipulating the parent, and checking the immediate children. Some developers write tests to check that triggering a certain input calls a certain internal function. Our approach is better because we check whether components communicate correctly with each other through the same props used in the actual application.

You can now test whether components can pass data around, but sometimes you are more concerned about whether they are rendering the correct UI. Since using detailed assertions about every element becomes very time consuming, we’ll speed up the process with snapshots.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.183.210