39

Unit Testing React Components

 5 years ago
source link: https://www.tuicool.com/articles/hit/be6vUfe
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Unit testing is a great discipline which can lead to 40%-80% reductions in production bug density . Unit testing also has several other important benefits:

console.log()

But some things are easier to unit test than others. Specifically, unit tests work great forpure functions: Functions which given the same input, always return the same output, and have no side-effects.

Often, UI components don’t fall into that category of things which are easy to unit test, which makes it harder to stick to the discipline of TDD: Writing tests first.

Writing tests first is necessary to achieve some of the benefits I listed: architecture improvements, better developer experience design, and quicker feedback as you’re developing your app. It takes discipline and practice to train yourself to use TDD. Many developers prefer to tinker before they write tests, but if you don’t write tests first, you rob yourself of a lot of the best features of unit tests.

It’s worth the practice and discipline, though. TDD with unit tests can train you to write UI components which are far simpler, easier to maintain, and easier to compose and reuse with other components.

One recent innovation in my testing discipline is the development of the RITEway unit testing framework , which is a tiny wrapper around Tape that helps you write simpler, more maintainable tests.

No matter what framework you use, the following tips will help you write better, more testable, more readable, more composable React components:

  • Favor pure components for UI code: given same props, always render the same component. If you need state from the app, you can wrap those pure components with a container component which manages state and side-effects.
  • Isolate application logic/business rules in pure reducer functions.
  • Isolate side effects using container components.

Favor Pure Components

A pure component is a component which, given the same props, always renders the same UI, and has no side-effects. E.g.,

import React from 'react';
const Hello = ({ userName }) => (
  <div className="greeting">Hello, {userName}!</div>
);
export default Hello;

These kinds of components are generally very easy to test. You’ll need a way to select the component (in this case, we’re selecting by the greeting className ), and you'll need to know the expected output. To write pure component tests, I use render-component from RITEway .

To get started, install RITEway:

npm install --save-dev riteway

Internally, RITEway uses react-dom/server renderToStaticMarkup() and wraps the output in a Cheerio object for easy selections. If you're not using RITEway, you can do all that manually to create your own function to render React components to static markup you can query with Cheerio.

Once you have a render function to produce a Cheerio object from your markup, you can write component tests like this:

import { describe } from 'riteway';
import render from 'riteway/render-component';
import React from 'react';
import Hello from '../hello';
describe('Hello component', async assert => {
  const userName = 'Spiderman';
  const $ = render(<Hello userName={userName} />);
assert({
    given: 'a username',
    should: 'Render a greeting to the correct username.',
    actual: $('.greeting')
      .html()
      .trim(),
    expected: `Hello, ${userName}!`
  });
});

But that’s not very interesting. What if you need to test a stateful component, or a component with side-effects? That’s where TDD gets really interesting for React components, because the answer to that question is the same as the answer to another important question: “How can I make my React components more maintainable and easy to debug?”

The answer: Isolate your state and side-effects from your presentation components. You can do that by encapsulating your state and side-effect management in a container component, and then pass the state into a pure component through props.

But didn’t the hooks API make it so that we can have flat component hierarchies and forget about all that component nesting stuff? Well, not quite. It’s still a good idea to keep your code in three different buckets, and keep these buckets isolated from each other:

  • Display/UI Components
  • Program logic/business rules  — the stuff that deals with the problem you’re solving for the user.
  • Side effects (I/O, network, disk, etc.)

In my experience, if you keep the display/UI concerns separate from program logic and side-effects, it makes your life a lot easier. This rule of thumb has always held true for me, in every language and every framework I’ve ever used, including React with hooks.

Let’s demonstrate stateful components by building a click counter. First, we’ll build the UI component. It should display something like, “Clicks: 13” to tell you how many times a button has been clicked. The button will just say “Click”.

Unit tests for the display component are pretty easy. We really only need to test that the button gets rendered at all (we don’t care about what the label says — it may say different things in different lanugages, depending on user locale settings). We do want to make sure that the correct number of clicks gets displayed. Let’s write two tests: One for the button display, and one for the number of clicks to be renedered correctly.

When using TDD, I frequently use two different assertions to ensure that I’ve written the component so that the proper value is pulled from props. It’s possible to write a test so that you could hard-code the value in the function. To guard against that, you can write two tests which each test a different value.

In this case, we’ll create a component called <ClickCounter> , and that component will have a prop for the click count, called clicks . To use it, simply render the component and set the clicks prop to the number of clicks you want it to display.

Let’s look at a pair of unit tests that could ensure we’re pulling the click count from props. Let’s create a new file, click-counter/click-counter-component.test.js :

import { describe } from 'riteway';
import render from 'riteway/render-component';
import React from 'react';
import ClickCounter from '../click-counter/click-counter-component';
describe('ClickCounter component', async assert => {
  const createCounter = clickCount =>
    render(<ClickCounter clicks={ clickCount } />)
  ;
{
    const count = 3;
    const $ = createCounter(count);
assert({
      given: 'a click count',
      should: 'render the correct number of clicks.',
      actual: parseInt($('.clicks-count').html().trim(), 10),
      expected: count
    });
  }
{
    const count = 5;
    const $ = createCounter(count);
assert({
      given: 'a click count',
      should: 'render the correct number of clicks.',
      actual: parseInt($('.clicks-count').html().trim(), 10),
      expected: count
    });
  }
});

I like to create little factory functions to make it easier to write tests. In this case, createCounter will take a number of clicks to inject, and return a rendered component using that number of clicks:

const createCounter = clickCount =>
  render(<ClickCounter clicks={ clickCount } />)
;

With the tests written, it’s time to create our ClickCounter display component. I've colocated mine in the same folder with my test file, with the name, click-counter-component.js . First, let's write a component fragment and watch our test fail:

import React, { Fragment } from 'react';
export default () =>
  <Fragment>
  </Fragment>
;

If we save and run our tests, we’ll get a TypeError , which currently triggers Node's UnhandledPromiseRejectionWarning  — eventually, Node will stop with the irritating warnings with the extra paragraph of DeprecationWarning and just throw an UnhandledPromiseRejectionError , instead. We get the TypeError because our selection returns null , and we're trying to run  .trim() on it. Let's fix that by rendering the expected selector:

import React, { Fragment } from 'react';
export default () =>
  <Fragment>
    <span className="clicks-count">3</span>
  </Fragment>
;

Great. Now we should have one passing test, and one failing test:

# ClickCounter component
ok 2 Given a click count: should render the correct number of clicks.
not ok 3 Given a click count: should render the correct number of clicks.
  ---
    operator: deepEqual
    expected: 5
    actual:   3
    at: assert (/home/eric/dev/react-pure-component-starter/node_modules/riteway/source/riteway.js:15:10)
...

To fix it, take the count as a prop, and use the live prop value in the JSX:

import React, { Fragment } from 'react';
export default ({ clicks }) =>
  <Fragment>
    <span className="clicks-count">{ clicks }</span>
  </Fragment>
;

Now our whole test suite is passing:

TAP version 13
# Hello component
ok 1 Given a username: should Render a greeting to the correct username.
# ClickCounter component
ok 2 Given a click count: should render the correct number of clicks.
ok 3 Given a click count: should render the correct number of clicks.
1..3
# tests 3
# pass  3
# ok

Time to test the button. First, add the test and watch it fail (TDD style):

{
  const $ = createCounter(0);
assert({
    given: 'expected props',
    should: 'render the click button.',
    actual: $('.click-button').length,
    expected: 1
  });
}

This produces a failing test:

not ok 4 Given expected props: should render the click button
  ---
    operator: deepEqual
    expected: 1
    actual:   0
...

Now we’ll implement the click button:

export default ({ clicks }) =>
  <Fragment>
    <span className="clicks-count">{ clicks }</span>
    <button className="click-button">Click</button>
  </Fragment>
;

And the test passes:

TAP version 13
# Hello component
ok 1 Given a username: should Render a greeting to the correct username.
# ClickCounter component
ok 2 Given a click count: should render the correct number of clicks.
ok 3 Given a click count: should render the correct number of clicks.
ok 4 Given expected props: should render the click button.
1..4
# tests 4
# pass  4
# ok

Now we just need to implement the state logic and hook up the event handler.

Unit Testing Stateful Components

The approach I’m going to show you is probably overkill for a click counter, but most apps are far more complex than a click counter. State is often saved to database or shared between components. The popular refrain in the React community is to start with local component state and then lift it to a parent component or global app state on an as-needed basis.

It turns out that if you start your local component state management with pure functions, that process is easier to manage later. For this and other reasons (like React lifecycle confusion, state consistency, avoiding common bugs), I like to implement my state management using pure reducer functions. For local component state, you can then import them and apply the useReducer React hook.

If you need to lift the state to be managed by a state manager like Redux, you’re already half way there before you even start: Unit tests and all.

First, I’ll create a new test file for state reducers. I’ll colocate this in the same folder, but use a different file. I’m calling this one click-counter/click-counter-reducer.test.js :

import { describe } from 'riteway';
import { reducer, click } from '../click-counter/click-counter-reducer';
describe('click counter reducer', async assert => {
  assert({
    given: 'no arguments',
    should: 'return the valid initial state',
    actual: reducer(),
    expected: 0
  });
});

I always start with an assertion to ensure that the reducer will produce a valid initial state. If you later decide to use Redux, it will call each reducer with no state in order to produce the initial state for the store. This also makes it really easy to create a valid initial state any time you need one for unit testing purposes, or to initialize your component state.

Of course, we’ll need to create a corresponding reducer file. I’m calling it click-counter/click-counter-reducer.js :

const click = () => {};
const reducer = () => {};
export { reducer, click };

I’m starting by simply exporting an empty reducer and action creator. To learn more about the important role of things like action creators and selectors, read “10 Tips for Better Redux Architecture” . We’re not going to take the deep dive into React/Redux architecture patterns right now, but an understanding of the topic will go a long way towards understanding what we’re doing here, even if you are not going to use the Redux library.

First, we’ll watch the test fail:

# click counter reducer
not ok 5 Given no arguments: should return the valid initial state
  ---
    operator: deepEqual
    expected: 0
    actual:   undefined

Now let’s make the test pass:

const reducer = () => 0;

The initial value test will pass now, but it’s time to add more meaningful tests:

assert({
    given: 'initial state and a click action',
    should: 'add a click to the count',
    actual: reducer(undefined, click()),
    expected: 1
  });
assert({
    given: 'a click count and a click action',
    should: 'add a click to the count',
    actual: reducer(3, click()),
    expected: 4
  });

Watch the tests fail (both return 0 when they should return 1 and 4 , respectively). Then implement the fix.

Notice that I’m using the click() action creator as the reducer's public API. In my opinion, you should think of the reducer as something that your application does not interact directly with. Instead, it uses action creators and selectors as the public API for the reducer.

I also don't write separate unit tests for action creators and selectors. I always test them in combination with the reducer. Testing the reducer is testing the action creators and selectors, and vice versa. If you follow this rule of thumb, you'll need fewer tests, but still achieve the same test and case coverage as you would if you tested them independently.

const click = () => ({
  type: 'click-counter/click',
});
const reducer = (state = 0, { type } = {}) => {
  switch (type) {
    case click().type: return state + 1;
    default: return state;
  }
};
export { reducer, click };

Now all the unit tests will pass:

TAP version 13
# Hello component
ok 1 Given a username: should Render a greeting to the correct username.
# ClickCounter component
ok 2 Given a click count: should render the correct number of clicks.
ok 3 Given a click count: should render the correct number of clicks.
ok 4 Given expected props: should render the click button.
# click counter reducer
ok 5 Given no arguments: should return the valid initial state
ok 6 Given initial state and a click action: should add a click to the count
ok 7 Given a click count and a click action: should add a click to the count
1..7
# tests 7
# pass  7
# ok

Just one more step: Connecting our behavior to our component. We can do that with a container component. I’ll just call that index.js and colocate it with the other files. It should look something like this:

import React, { useReducer } from 'react';
import Counter from './click-counter-component';
import { reducer, click } from './click-counter-reducer';
export default () => {
  const [clicks, dispatch] = useReducer(reducer, reducer());
  return <Counter
    clicks={ clicks }
    onClick={() => dispatch(click())}
  />;
};

That’s it. This component’s only job is to connect our state management and pass the state in as props to our unit-tested pure component. To test it, load the app in your browser and click the click button.

Up until now we haven’t looked at the component in the browser or done any kind of styling. Just to clarify what we’re counting, I’ll add a label and some space to the ClickCounter component. I'll also hook up the onClick function. Now the code looks like this:

import React, { Fragment } from 'react';
export default ({ clicks, onClick }) =>
  <Fragment>
    Clicks: <span className="clicks-count">{ clicks }</span> 
    <button className="click-button" onClick={onClick}>Click</button>
  </Fragment>
;

And all the unit tests still pass.

What about tests for the container component? I don’t unit test container components. Instead, I use functional tests, which run in-browser and simulate user interactions with the actual UI, running end-to-end. You need both kinds of tests (unit and functional) in your application, and unit testing your container components (which are mostly connective/wiring components like the one that wires up our reducer, above) would be too redundant with functional tests for my taste, and not particularly easy to unit test properly. Often, you’d have to mock various container component dependencies to get them to work.

In the meantime, we’ve unit tested all the important units that don’t depend on side-effects: We’re testing that the correct data gets rendered and that the state is managed correctly. You should also load the component in the browser and see for yourself that the button works and the UI responds.

Implementing functional/e2e tests for React is the same as implementing them for any other framework. I won’t go into them here, but check out TestCafe , TestCafe Studio and Cypress.io for e2e testing without the Selenium dance.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK