Skip to content
Contact

Testing React components

Testing React components

If you’re writing your front-ends in React, are you writing tests?

Testing is hard. We all know it. It’s practically a separate skill that needs to be constantly trained and improved in addition to your usual programming abilities. That’s why people find it scary, especially when they’re coming from back-end environment. In this blog post, I want to prove to you that testing your React frontend may actually be easy to set up and quite pleasant to do.

What this blog post does not touch is all the rules and best practices when testing. I’m afraid there are entire books dedicated to this topic, and it takes years of practice to start noticing certain patterns on your own. I will, however, try to help you with beginning your journey.

Before we begin, let’s discuss our tooling. We are going to use Jest as a platform. The biggest advantage of this one over other tools is that it’s (almost) zero configuration — we can drop it into our project without extra complications, and it will take care of configuring and compiling our tests with Babel for us. If you’re using create-react-app, it’s already added to the project, so there’s one less thing for you to worry about ;)

Other than Jest, I recommend amazing Sinon.js for all your stubbing, mocking and XHR faking needs and Enzyme for rendering your React components and poking them, checking how they work and why. The last ingredient of our secret testing sauce is enzyme-to-json — it facilitates seamless integration between Jest snapshot testing (more about that in a moment) and Enzyme.

Add all of these to your project and keep on reading:

yarn add --dev jest sinon enzyme enzyme-to-json

Different kinds of rendering

Before we start our test writing adventure, there’s something you need to understand first. When we are testing our React components, we are not actually rendering them in the browser (OK, we do, but only in very specific kinds of tests). Often we don’t even want to touch the DOM and generate HTML as it doesn’t make sense and only makes our tests slower.

There are three ways we can render our components using Enzyme:

Shallow rendering

Shallow rendering is the kind of rendering that you want to use as often as possible. It takes your component and renders only the first layer, the native components (like <div></div>) and placeholders for other components with passed props but without actually rendering them (or running any lifecycle methods). It also doesn’t touch DOM and opts for returning simple objects instead.

This is the best kind of rendering you want in your tests — it’s the fastest, and it keeps your tests decoupled from their children components, so if any of those children components break, rest of the tests are not making it harder to pinpoint the problem.

Since this kind of rendering does not generate DOM, it does not run all lifecycle methods, so you don’t have to worry about your componentDidMountcrashing. This also means that if for some reason you’re doing something other than additional manipulation of rendered output (for ex. fetching data in componentDidMount instead of componentWillMount), it will never get executed in your tests.

The entire API is available in documentation, and you will probably want to keep this tab open until you’re confident enough that you remember what you need from it.

Full rendering (mount)

If for some reason (for example: because you’re testing a component that is wrapping external library, when you’re testing browser events or when you want componentDidMount to run) you need to access to full DOM API, Enzyme has your back. Full rendering using mount() renders your component and all of its children while using JSDOM (browserless Javascript implementation of DOM) to make sure all those extra manual addEventListener work properly.

You might be inclined to use this as often as possible but unless you have a very good cause you should avoid doing full mount. It’s much slower than shallow rendering, and it introduces coupling between your parent component test and its children. Imagine the situation where you always do a full mount and then introduce a bug in an <Icon/> component. Suddenly all your tests have crashed, and it’s much much harder to figure out where, when and why it happened.

The API is similar to shallow, and it’s also available in the documentation of Enzyme.

Static HTML rendering

This is the last kind of rendering. It uses ReactDOM static rendering underneath so it works like server-side rendering, but the result is then wrapped in Cheerio.js library to make it easier to traverse and analyse the HTML result.

In this case, only the example usage is in docs ,and you should consult cheerio documentation instead. I didn’t feel the need yet to use this kind of testing, but it might be useful in your particular case so it’s good to know that you have this option.

Unwrapping components

One of the things that most people starting with testing components is tripping on are components that are wrapped in HOC (Higher-Order Component). If you’re using something like Redux or MobX, you’re probably used to exporting your component wrapped in either connect() or @inject. Those functions work by creating an extra component above yours that passes extra data with React contexts.

Unfortunately, this will cause a problem when using shallow rendering because it will only render an empty container and not the thing we want to test.

How can we avoid this problem? There are three ways:

Dive!

The solution most people try at first is to use .dive() to render one component deeper. Regrettably, this solution may be very confusing and throw hard to debug errors as it does not respect context being passed from the wrapping component. I would avoid this if possible.

Use escape hatches

I think almost every library I’ve seen so far has implemented some kind of escape hatch to give you access to the original component. The problem with this solution is that you always need to remember what the escape hatch was and in which order it should be applied. For example, if you use withRouter and inject on one component, you would have to unwrap it like this:

const TestedButton = Button.wrappedComponent.WrappedComponent

As you may imagine, this is not a perfect solution, and it does not scale very well.

Export unwrapped component

My favourite method of dealing with this extra complexity is to export the component without any extra wrappers in addition to the one treated with injectconnectwithRouter etc.

The only real drawback is that we cannot use @decorators on entire class and we need to do the wrapping ourselves on export:

export MyComponent as UnwrappedMyComponent
export default inject('store')(withRouter(MyComponent))

I do not believe this to be a huge hassle (the export is only a bit more verbose), and it allows us to test actual components, not coupling with the rest of the system.

All right, we have the theory, we have the links to documentation, we’re ready to start writing tests.

But what exactly do we want to test? How do we want to do this? What kinds of tests are there?

Types of component tests

There are multiple naming conventions regarding tests so don’t treat the following list like the one and only possible way of categorisation.

Snapshot tests

These are the simplest to write tests that can be used for a quick (and brittle) way of having as much test coverage as possible. They work by serializing the result of rendering to a JSON file called snapshot and then, during future runs of the test, using it to compare with future render results to make sure they are unchanged.

import React from 'react'
import { shallow } from 'enzyme'
import toJson from 'enzyme-to-json'
import { UnwrappedButton } from 'components/button'
describe('Button', () => {
it('matches the snapshot', () => {
const component = shallow(<UnwrappedButton store=/>)
// this line is important
expect(toJson(component)).toMatchSnapshot()
})
})

I’ve mentioned that these tests are brittle. Because of the way they work, they are going to fail the moment anything in your component changes visually which means even fixing a typo or changing the className is going to break them. They are still very useful as a sanity check, especially when doing larger refactoring.

The other thing you need to be very careful about is that all props passed to your components will also be serialised. This means if you pass a huge object (like entire, non-mocked Store) during your tests, the resulting JSON might be huge which will make the test run (seemingly) forever - we had this problem where someone in a test passed a non-mocked store which kept an instance of HLS.js. The resulting JSON was 5 MB (!)

Rendering tests

Rendering tests are (in this case) tests that test… rendering. More specifically they’re the more precise version of snapshot tests that render a component and then poke it around to check if props were passed correctly and all UI elements necessary were rendered and are available.

import React from 'react'
import { shallow } from 'enzyme'
import toJson from 'enzyme-to-json'
import { UnwrappedButton } from 'components/button'
describe('Button', () => {
it('renders label', () => {
const component = shallow(<UnwrappedButton store=/>)
expect(component.find('button').text()).toEqual('foo')
})
})

They provide a better insight into what is happening inside the component, which means they’re more useful when working in teams; If someone needs to know how a component works, they can just look at these and behaviour (more in a moment) tests, and do an educated guess which is harder in case of snapshots.

Behaviour tests

The most important of all tests and the ones that you absolutely have to write, even if you slack off and ignore all the other categories are behaviour tests. They’re the bread and butter of your application test suite — they test how your application behaves when user interactions happen.

const sandbox = sinon.createSandbox()
describe('EmptyForm', () => {
afterAll(() => sandbox.restore())
 it('triggers form object onSuccess on submit', () => {
const component = shallow(<EmptyForm/>)
const instance = component.instance()
const stub = sandbox.stub(instance.form, 'onSuccess')
.returns(true)
 component.find('form').simulate('submit')
expect(stub.calledOnce).toBe(true)
})
})

Behavior tests mostly simulate browser events and focus on checking if event handlers are attached properly. Side effects, like network requests, timers etc. should be mocked/stubbed to avoid test coupling.

There is an important caveat when writing behaviour tests with Enzyme — you may think that .simulate() is simulating actual browser events, but that is not entirely true. What it does is it finds the event handler and calls it, passing any extra data we provide to it. It does not support things like event bubblingor calling onChange when simulating keypresses. If you need any of those advanced features, you need to code them yourself.

If it makes it easier to wrap your head around it, remember that those two are more or less equivalent:

component.simulate('change', { target: { value: 'abc' } })
component.prop('onChange')({ target: { value: 'abc' } })

Integration tests

Integration tests are testing communication between components. They are the ones that benefit most from the full mount as they need to actually run more than component in a nested tree and see how all parts fit in together. They are basically behaviour tests but for groups of components.

describe('Article', () => {
it('displays comments after clicking a show comments button', () => {
const article = mount(<Article store={mockedStore}/>)
article.find('ShowComments').simulate('click')
expect(article.find('Comment').length).toBe(5)
})
})

Most of the time every part of the integration test can also be written as a series of smaller behaviour tests with behaviour between them mocked. These tests should check if that mocked behaviour is actually connected properly.

System tests

The last on our list are system tests. They are very similar to integration tests, but instead of running in a simulated environment, they’re are running in an actual browser. They are also, unfortunately, the slowest of the bunch so it’s a good idea to separate them from your main test suite and launch for example only on CI server (instead of every time you change something).

import Nightmare from 'nightmare'
describe('App', () => {
it('renders the initial app', async () => {
const nightmare = Nightmare()
return nightmare
.goto('http://localhost:4000')
.type('#search_form_input_homepage', 'github nightmare')
.click('#search_button_homepage')
.wait('#r1-0 a.result__a')
.evaluate(() => document.querySelector('#r1-0 a.result__a').href)
.end()
.then((link) => {
expect(link).toBe('https://github.com/segmentio/nightmare')
})
})
});
});

The system tests require an extra library that takes care of opening your application and passing your commands to the browser. The two that I found most interesting are Nightwatch.js that uses Selenium and Nightmare that runs on Electron. Historically Selenium was usually a bit tricky to properly configure but a lot has changed since PhantomJS got abandoned and Chrome headless became the new standard so your mileage may vary.

Configuring and running system tests can be complicated depending on the environment, so I’ve released jest-str, a simple system test runner that contains preconfigured presets for popular boilerplates (at the moment of writing this blog post there are two — for create-react-app and razzle). If you want your favourite boilerplate to also get an official preset, feel free to send a PR :)

Great, we now know the different ways of testing components. If you’ve been careful with reading code examples, you probably get a general idea already of how we’re doing it but let’s get more specific.

Using Jest and Jest matchers

As you might’ve noticed already, Jest uses a spec-like syntax to define tests. They can be grouped using describe blocks for easier navigation, marking boundaries and behaviors being tested and for better error messages. The tests themselves are in it blocks and wrap tested values in expect().

There are 26 matchers which may seem overwhelming at first so when beginning your journey, focus on these 4:

  • toBe - checks for strict equality (===), useful for comparing numbers, true/false values, exact strings etc.
  • toEqual - performs deep value comparison, useful when checking for objects that have the same fields but are not necessarily the same object (ex. expect(myObj).toEqual({ foo: 1 }))
  • toThrow - checks if the function passed to expect() have thrown an exception (ex. expect(() => something()).toThrow(/fail/)), keep in mind to always create an anonymous function in expect() to avoid problems with scoping
  • toMatchSnapshot - used for snapshot testing

After your tests get more complex and you get the general gist of it, you will notice that you need more than what’s above. It might be then very helpful to keep this documentation page around.

Using spies and stubs and sandboxes

Sinon.js gives us tools to observe the inner workings of our application and change it when necessary. Those tools are spies and stubs.

Spies

Spies are functions that keep track of how many times and with what arguments they were called. They can also be used as a wrapper around the original function and return its value. Spies are most useful for testing behaviours, for example, to check if the component has triggered an action in store.

I didn’t want to artificially make this blog post any longer, so just check out the documentation to see exactly everything that’s possible with them.

Stubs

Stubs are spies with controlled behaviour. They include the same API as spies but they can also be used to return specific values, run their own fake function, call original function (or wrap it), throw errors, reject promises etc.

They are most useful when disabling parts of the application we do not want to test at the moment (XHR, complex operations, side effects etc), or when we want to make sure a certain path in code is being run (like making sure an if()somewhere gets true/false or when testing error handling).

Sandbox

Mocking functions inside stores is nice but what about test isolation? If we replace a method inside a pre-existing object with a stub, it may leak to the other tests, making them dependent on the order they were run. This is not an ideal scenario but fortunately, Sinon implements sandboxes.

A Sandbox is basically an orchestrating object that keeps track of all the spies and stubs and allows us to reset them after every call. It’s usually set up like this:

const sandbox = sinon.createSandbox()
describe('something', () => {
afterEach(() => sandbox.restore())
// ...
})

When using sandbox, remember to create spies and stubs using sandbox.spy() / sandbox.stub() instead of sinon.spy() and sinon.stub().

Stubbing network requests

There are multiple techniques to achieve that, depending on the library you’re using for network requests.

If you’re using something like $.ajax then check out Sinon’s fake XHR server.

For axios you might be inclined to use moxios, the official testing library from the same author. I wouldn’t recommend it as moxios is rarely updated and lacks some features that can be necessary for more complex flows. Personally, I use axios-mock-adapter as it has much better and more powerful API.

Finally, if you’re using Fetch API, consider fetch-mock. It has a feature set similar to axios-mock-adapter. Keep in mind that fetch() is a browser API and you will need a polyfill like isomorphic-fetch to make it work!

General guidelines on testing

There are a couple of things that you should remember during the tests. If you don’t, you will trip and hurt yourself and drop testing front-ends again.

Avoid testing wrapped components. I mean it, always sure your components are unwrapped, otherwise you will encounter a world of pain and arcane errors. This is where most of the people I’ve talked with bounced off testing altogether.

The only good moment to test wrapped components is during integration tests with full mount.

Mock your stores whenever possible. It’s tempting to just instantiate your entire Store class in tests but it introduces tight coupling to the current state of your store and makes your tests both tightly coupled to it and, in case of complex apps, significantly slower.

Keep tests simple. You may be tempted to test for 10 different things in one test case (it will be faster!) but it will once again make your tests more brittle and prone to failing. It’s better to have laser focus so every time your test suite fails, you have a clear message of what went wrong, where and why.

Group your tests. There’s nothing worse than a huge file full of ungrouped, unordered tests. Be careful not to overdo it though, one or two levels of nesting are usually more than enough.

Keep your tests in one describe() block. It will make your life easier with before / after blocks, especially when it comes to sandboxing or stubbing requests.

And that’s it! As you can see, testing your application components is not that hard and hopefully, you should have a better idea on where to begin now :)

Keep your code bugs free!


 

By Michał Matyas

Web Developer