With React and the ecosystem of testing tools that have emerged around it, it’s finally possible to build robust, scalable tests that provide strong guarantees on code correctness.
When we decided to start writing UI tests for our app, we found that there weren’t any great resources that explained the various techniques for React testing. The goal of this series is to discuss these techniques and provide an overview of how to get started.
In modern JavaScript front ends, there are 3 types of tests that we might want to write:
- Unit tests: these verify the behavior of individual components or modules. There are a number of tools and libraries that make writing these tests easier.
- Application tests: applications and business logic are hard to fully test with unit tests. Application tests (often called “integration tests”) test your entire application code, often with a mocked-out API.
- End-to-end tests: if you want to test your entire application (front end and back end), end-to-end tests allow you to make assertions that your entire system works as expected.
In this post, I’ll talk about unit tests and when / how to write them.
Unit Testing
In React, unit tests fall under a few different buckets:
- Logic tests
- Component tests
- Storybook tests
Imagine that we’re building a “calculator” app. Each of these tests would serve a slightly different purpose:
- Logic tests: does the calculator properly evaluate an equation and return the correct result?
- Component tests: does a number show up in the calculator which I click a button?
- Storybook tests: does the calculator look correct when the user is adding two numbers?
Logic tests
Logic tests are the most straightforward unit tests. The simplest case might look something like this:
import { expect } from 'chai';
function addTwoNumbers(a, b) {
return a + b;
}
describe('addTwoNumbers', () => {
it('should add two numbers', () => {
expect(addTwoNumber(2, 3)).to.equal(5);
});
});
These are usually really quick to write, and are quite useful when working on gnarly code that has a lot of edge cases. It’s important not to go overboard with these tests — you should only write tests for code with a stable API. If the function is constantly changing in purpose or inputs, the guarantees of the test will become meaningless.
To set up logic tests in your front end, I’d recommend using Jest or Mocha, along with Chai to make test assertions:
- Jest: https://github.com/facebook/jest
- Mocha: https://mochajs.org/
- Chai: http://chaijs.com/
Component tests
React’s component model is quite convenient for writing tests. Instead of having to test an entire app with an integration test, component tests allow us to test individual components in isolation. Moreover, React’s virtual event model lets us perform “actions” on a component without the browser environment! Here’s an example integration test which uses AirBnB’s enzyme library:
class MyComponent extends Component {
state = {
counter: 0,
};
render() {
return (
<div>
{this.state.counter}
<button onClick={() => this.setState({ counter: this.state.counter + 1 })} />
</div>
);
}
}
describe('MyComponent', () => {
it('should increment the counter when the button is pressed', () => {
const wrapper = shallow(<MyComponent />);
expect(wrapper.text()).to.contain(0);
wrapper.find('button').simulate('click');
expect(wrapper.text()).to.contain(1);
});
});
You can play around with enzyme and component testing at this Runkit: https://runkit.com/arbesfeld/enzyme-example
Like logic tests, it’s important to find the correct API boundaries to test. It’s not worth writing tests for components that are constantly changing. Also, simple pure-functional components are not that useful to test: they rarely have bugs or issues. I’d recommend writing component tests when you are building a UI library of complex components, like multi-selects, type-ahead search boxes, etc.
Storybook tests
react-storybook and other “UI Dev Environments” allow you to build components in a self-contained dev environment, and then persist “Stories” of your components which make it easy for other developers to iterate on the component.
Some developers would argue that these are not real tests, but I feel like they have the same purpose as tests. By codifying all of the possible states of a component, it makes it easier for other developers to improve the component, and use the component in other parts of an application.
If you’re interested in react-storybooks, here are a few other good resources for digging in:
- UI Component Playbook provides a great overview of how to engineer frontends with components.
- Arunoda introducing react-storbooks: https://voice.kadira.io/introducing-react-storybook-ec27f28de1e2
- For advances users, Storyshots (https://github.com/storybooks/storyshots) lets you write actual tests from your storybook stories.
Should I write frontend unit tests?
This is a common question that I hear when discussing React engineering. To make this decision there are often a few factors at play:
- How complex is the component logic? If the test is as long as the component code, it’s probably not worth testing.
- How likely is the component to change? Is this feature / component going to be evolving over time? If so, it’s probably better to consider integration tests.
- Do tests help me write the component? Sometimes writing tests before coding a component is actually really helpful in speeding up development.
- How robust does the product have to be? Depending on the stage of your product, you might have very high reliability guarantees. Unit tests will almost certainly reduce the likelihood of bugs over time, which could be impactful for the business.
Thanks for listening — and happy unit testing!
Get set up with LogRocket's modern React error tracking in minutes:
- Visit https://logrocket.com/signup/ to get an app ID
-
Install LogRocket via npm or script tag.
LogRocket.init()
must be called client-side, not server-side$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
- (Optional) Install plugins for deeper integrations with your stack:
- Redux middleware
- NgRx middleware
- Vuex plugin