Unit Testing React Components

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 ComponentsProgram 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 componentok 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 componentok 1 Given a username: should Render a greeting to the correct username.

# ClickCounter componentok 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# okTime 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 componentok 1 Given a username: should Render a greeting to the correct username.

# ClickCounter componentok 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# okNow we just need to implement the state logic and hook up the event handler.

Unit Testing Stateful ComponentsThe 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 reducernot ok 5 Given no arguments: should return the valid initial state — operator: deepEqual expected: 0 actual: undefinedNow 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 componentok 1 Given a username: should Render a greeting to the correct username.

# ClickCounter componentok 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 reducerok 5 Given no arguments: should return the valid initial stateok 6 Given initial state and a click action: should add a click to the countok 7 Given a click count and a click action: should add a click to the count1.

7# tests 7# pass 7# okJust 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>&nbsp; <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.

Eric Elliott is a distributed systems expert and author of the books, “Composing Software” and “Programming JavaScript Applications”.

As co-founder of DevAnywhere.

io, he teaches developers the skills they need to work remotely and embrace work/life balance.

He builds and advises development teams for crypto projects, and has contributed to software experiences for Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC, and top recording artists including Usher, Frank Ocean, Metallica, and many more.

He enjoys a remote lifestyle with the most beautiful woman in the world.

.

. More details

Leave a Reply