Test Driven Development in React with Jest and Enzyme

Exploring snapshots, mock functions simulation testing and more

Introduction: How do you Develop?

This talk covers a range of testing tools that Enzyme and Jest offer for React, ranging from snapshot testing, mock functions, simulating events, and how to test component functions, props and state. We will also explore other aspects of testing including abstract-ability and logic isolation within your React projects, and finally wrap up with a testing checklist to adhere to when designing your own tests.

Enzyme and Jest are critical tools that can ensure a high level of stability to your code from your first deployment, and can be used whether you are adopting a Test Driven Development or Behaviour Driven Development strategy. Before jumping into the tools let’s review the differences between these two development approaches in an abstract way.

Behavioural Driven Development

How much time do you spend testing your apps? Is it done after a milestone or at intervals as you are developing components? The former approach gives precedence to the behaviour of the app, prioritising business logic completion over component stability. Your testing suites will be coded once the behavioural implementation is complete, the results of such tests determining the best route to refactoring your code or fixing any holes along the way.

What this approach is doing in essence is integration testing your components before writing your unit tests — or any formal tests at all using a sophisticated framework like Jest. This indeed yields a faster turnaround, and is quite widely adopted. Why is this the case? It could be because business meetings rarely discuss apps from a test driven angle — the focus is always on the end result or the intended behaviour set out to be achieved. It is arguably natural to lean towards BDD given these factors; meeting notes will be totally behavioural-based, with a tight schedule to get the job done.

Now, this may be great for rapid prototyping or creating a proof of concept — but for apps with a sizeable codebase, complex integrations with a range of services, or simply a large team concurrently contributing (a blockchain being a great example for all of these cases), a test-first approach to developing makes a lot of sense, in the form of Test Driven Development — and this method also happens to be naturally suited for React development.

Test Driven Development

Whether you are developing a front end user experience or an NPM package that thousands of developers and millions of end-users rely on, stability and reliability will be among your highest priorities for your codebase. What TDD does is promote clean and functional code, prompting the developer to consider things like how the component will be integrated, how reusable and how refactor-able it is.

TDD promotes developing with intent rather than procedure. By doing this, lots of complexity of stitching an entire experience together is removed. React components already do a great job at compartmentalising logic, making them suited to TDD whereby unit tests and integration testing can be carried out on a per-component basis.

Ultimately, TDD aims to solve the problem of delivering buggy software to the end consumer by treating the testing procedure as part of the development process, rather than a job to do at the end of a sprint. Minimising bugs is of course in the interest of an entire organisation, and not just the dev team. In other words, testing should be something everyone cares about, not only developers.

This is all very abstract, so let’s delve into React specific TDD to get a feel of what can be done here.

This article builds on some basic knowledge of Enzyme and Jest. If you would like to familiarise yourself with Jest and Enzyme at this stage, please refer to my introductory article on the packages:


Structuring React Components for Testing

React developers accustomed to developing components will know that component logic can build up very fast as state changes, API calls and other logic like mappings are introduced. What we want to do to this logic is ensure that it works under all the circumstances it will be used with. To do this, we want to achieve 2 things:

1. Test the logic in an isolated manor

Abstracting code becomes a primary concern in TDD, which also indirectly promotes principles such as DRY (Don’t Repeat Yourself) for maximum component reusability. This is easily achieved in React with the flexibility of components.

Consider an example project, a navigation app with a <SideBar /> and <TopBar /> component — both of which use an <Item /> component for navigation links. By separating each component (and their dedicated styling), we are isolating the logic as well as the tests associated with each component.

Remember, TDD with component isolation in mind would limit a component’s purpose to do just one thing, and reject the notion of it doing anything else but that thing.

Check out this hypothetical navigation app project structure:

// @rossbulat/nav package
src/
Item/
index.js
index.scss
SideBar/
index.js
index.scss
TopBar/
index.js
index.scss
tests/
Item.test.js
nav.test.js
SideBar.test.js
TopBar.test.js
// @rossbulat/app
src/
App.js
api.js
Home/
index.js
index.scss
tests/
app.test.js
api.test.js
// src/Home/index.js
import { SideBar, TopBar } from '@rossbulat/nav';

For ultimate abstract-ability, the navigation components actually come as a package, @rossbulat/nav, allowing me to include them in more projects to serve navigation needs. This warrants the usage of abstract component names like <Item /> as well as every component to be listed one level down under src/. With these components totally isolated, and with the testing suite embedded within the package, our navigation is much easier to manage.

Separating both <SideBar /> and <TopBar /> ensures that I can test each component independently. Not only this, each navigation link consists of an <Item /> component; this component may include props like title, link, icon, and more — all of which can be tested in an isolated manor.

As well as my separate component tests, I also have a nav.test.js file that can include my integration tests, or all 3 components working together, to make sure they adhere to an expected snapshot — perhaps checking how many <li> tags are nested within <SideBar /> markup to coincide with how many <Item /> components are present. We will visit snapshots further down.

My API is also completely separate from other components within my main app project; the api.test.js file is specifically for API calls, which will no doubt include various mock functions to serve fake API calls. Again, we will explore mocks further down this talk.

2. Test the code within a range of scenarios

This can only be achieved if the prior condition of abstracting logic is met, whereby an extensible range of props can be passed into the component, or in the case of integration testing, an extensible number of other components that work together.

With the power of isolation in mind, let’s next explore some tangible code examples of testing React components.

Perhaps the simplest of ways to test a React component is to compare the result of a render function with a snapshot file. Jest provide us with tools necessary for doing exactly this.

Using Snapshots with Jest

Using Jest’s terminology, snapshot testing is a useful feature to make sure that your markup does not unexpectedly change, and equally as true, makes sure that render() outputs what you intended it to.

Testing snapshots against components can be done with Enzyme methods (which we will see below) but can also be used with the react-test-renderer package (extremely popular with over 2 million weekly downloads at publishing time). Add it to your project via NPM or yarn:

yarn add react-test-renderer

A snapshot file is automatically generated when the toMatchSnapshot() method is called within your tests, often in conjunction with expect() in the following form:

expect(<component>).toMatchSnapshot();

The contents of a snapshot file mostly consists of markup that represents the expected output of your React component. Here is an example .snap file that Facebook have provided, that tests different link states.

Even though we would not manually edit a snapshot file under normal circumstances, they are easy to read by design for the purpose of code reviewing, and are designed to be added to source control — commit your snapshot files with your code updates.

Let’s visit a practical example of a snapshot test — I will test whether my <Item /> component from earlier renders correctly. Note that running the test for the first time will generate the snapshot file:

import React from 'react'
import { Item } from '@rossbulat/nav'
import renderer from 'react-test-renderer'
import icon from './img/ross.png'
describe('testing navigation', () => {
it('renders correctly', () => {
const item = renderer
.create(
<Item
link="https://rossbulat.co"
text="My Homepage"
icon={icon} />
).toJSON();
  expect(item).toMatchSnapshot();
});
});

This next example using Enzymes shallow() method, in this case testing whether my <SideBar /> component will render correctly against a snapshot:

...
it('sidebar should render correctly', () => {
const sidebar = shallow(<SideBar />);
expect(sidebar).toMatchSnapshot();
});

I could also test whether <SideBar /> renders correctly given an array of links:

it('sidebar should render links correctly', () => {
const links =
[{link: 'https://rossbulat.co', text: 'My Homepage'},
{link: 'https://medium.com/@rossbulat', text: 'Medium'}];
   const component = shallow(<MyComponent links={links} />);
expect(component).toMatchSnapshot();
});

This test will generate another snapshot. Concretely, each of my tests will generate a different snapshot file, even if the same component is being used.

If you are running your tests in watch mode, with yarn test --watch, followed by pressing i for interactive snapshot mode, failed snapshots will pop up as they happen and the exact failure will be documented, allowing you to troubleshoot inconsistencies.

Snapshot files are generated in a __snapshots__ folder under your test directories, all files of which will be named <test_name>.snap.

Note: The package used to convert Javascript objects into string representations within these .snap files is pretty-format. The package is just another NPM package, and can be used in your own projects where you wish to debug or document Javascript implementations.

If after you have updated your UI and wish to update your snapshots, we have a handy command to do just that. Run the following in your project directory to update all your snapshots:

jest --updateSnapshot

You will want to update your snapshots as you are enhancing your components. Make sure your initial snapshot outputs are as expected before running subsequent tests against them.

For the full documentation and feature set of snapshots in Jest, visit this page.

Simulating Events

Simulating events such as clicks and inputting text can be done within our tests using Enzyme’s simulate() method. For example, let’s say I wanted to test the result of clicking a button inside a component. This can be done like so:

it('should update form submitted state with button click', () => {
const component = mount(<RegistrationForm />);
component
.find('button#submit_form')
.simulate('click');

expect(component.state('form_submitted')).toEqual(true);
component.unmount();
});

Here we are testing a component, <RegistrationForm />, and the resulting state after clicking the button with id #submit_form. As demonstrated, component state can be accessed component.state('<key>'), and then compared with a matcher with expect().

In reality, we would probably need a form to be filled before submitting a registration form, at least with an email address or phone number. Can we easily test input values? Yes we can, like so:

component
.find('#name')
.simulate('change', { target: { value: 'Ross' } });

Similarly, we can also toggle checkboxes with simulate():

component
.find('#agreetoterms')
.simulate('change', {target: {checked: true}});

And even simulate a key press via a keycode (refer to keycodes here):

component.find('#input').simulate('keydown', { keyCode: 70 });

You may also wish to call component methods before testing state, snapshots or anything else. We can access component methods by its .instance() property:

const component = shallow(<MyComponent />);
const result = component.instance().callMethod();

Using Mock Functions

As the name suggests, Mock functions allow us to re-implement a function, stripping away logic for the purpose of capturing calls to the function, testing parameters, and testing return values.

The simplest way to use mock functions is to simply define an empty function and put it in place of an actual function within your tests. The benefit of doing this is tracking whether the function was actually called. (How many times have you been debugging and noticed that a particular function was not actually called, leading to the error?).

We can define such a function by using jest.fn(). This is how we would replace an onClick handler with an empty mock function, and test whether it has been called:

//define empty mock function 
const fnClick = jest.fn();
describe('click events', () => {
it('button click should show menu', () => {
    //replace actual function with mock function
const component = shallow(<MyButton onClick={fnClick} />);

//simulate a click
component
.find('button#btn_open_menu')
.simulate('click');

//check if function was called
expect(fnClick).toHaveBeenCalled();
});
});

The matcher .toHaveBeenCalled() is only one of a few we can test with mocks. Here are a few more:

// Test how many times the function is called
expect(fnClick.mock.calls.length).toBe(3);
// Test the values passed as arguments
// The second argument of the third call to the function was 'yes'
expect(mockCallback.mock.calls[2][1]).toBe('yes');
// Test return values
// The return value of the second call to the function was true
expect(mockCallback.mock.results[1].value).toBe(true);

Here we have tapped into the .mock property of a mock function, giving us access to information about how the function has been called, and what has been returned. Read more about it here.

Injecting Mock Functions

Another cool thing we can do with mocks is inject them in a test whenever we wish to retrieve the return value, with console.log():

const myMock = jest.fn();
console.log(myMock('return this string'));
// > return this string

We can also direct a mock function to return specific values before calling it within the test. This is handy to simulate a range of return values as proof your app can handle them:

// return `true` for first call, 
// return `false` for the second call
// return a string 'hello mock' for the third call
myMock.mockReturnValueOnce(true)
.mockReturnValueOnce(false)
.mockReturnValueOnce('hello mock');
//call the mock three times to witness the results:
console.log(myMock(), myMock(), myMock());

This is useful for retrieving data and simulating form submissions where there is arbitrary data being supplied. Test whether your app can handle an undefined value for example, along with a range of string lengths for input fields, as well as unexpected values, e.g. An object where a boolean value is expected.

Imagine a scenario where a JSON string {result: true} is returned instead of true. The JSON string itself is s truthy value, with a value of {result: false} also evaluating as truthy. Testing return values with mocks will ensure these scenarios do not happen.

Mock functions can be used in conjunction with everything we have explored so far, including snapshots and simulating events. You will find your tests becoming more sophisticated as more scenarios are covered. You are of course free to actually implement mock functions:

const myMock = jest.fn(() => {
...
});

The general rule of doing so is to not introduce arbitrary logic to the original function. What if one implementation is not enough — what if we would like to test multiple implementations of the same function? This can be done with mockImplementationOnce(() => {...}), taking the implementation itself as an argument:

//define implementations
const myMockFn = jest.fn(() => 'default return value')   
.mockImplementationOnce(() => {
...
return 'first implementation';
})
.mockImplementationOnce(() => {
...
return 'second implementation';
});
//call function
console.log(myMockFn(), myMockFn());
//any calls hereafter will refer to the default implementation.

Check out more on mock implementations here.

Modules can also be mocked using jest.mock(). Think database calls or API requests that are slow and fragile — things could break quite easily making for an unreliable test. Jest provide a simple example of mocking the axios module to overwrite API calls, but this is the general gist of how to mock a module function:

// import the module to mock
import axios from 'axios';
// wrap the module in jest.mock()
jest.mock('axios');
test('should fetch users', () => {
const users = [{first_name: 'Ross'}];
const resp = {data: users};
  // append .mockResolvedValue(<return value>) to the module method
axios.get.mockResolvedValue(resp);
  // carry out your test
return expect(resp.data).toEqual(users));
});

This is quite an elegant solution to bypassing module methods; by overwriting the functionality with Jest — .mockResolvedValue(<return value>) allows us to do exactly this.

Debugging components

It is worth mentioning here that if you wish to log a component’s render() output in HTML like fashion, use the debug() method of a wrapper:

const component = shallow(<MyComponent />);
console.log(component.debug());

Debugging components within your tests can quickly pinpoint why a test is failing.

Summary

In conjunction with my original introduction on testing, we have covered enough in this talk to get into serious React testing suites, with a range of methods that when used together can provide a comprehensive means of testing components.

Have we touched everything? No — but these powerful tools will kickstart your testing suite with Jest and Enzyme:

  • Snapshots: allowing you to compare render() output of components to an expected result.
  • Simulations: carry out tests that involve the events you would expect in the browser — click events, form inputs and function calls.
  • Mock functions: Test arguments and return values with empty mock functions, or carry out a range of implementations to test the expected result. Also, overwrite fragile module methods that rely on external data sources, or involve heavy processing that would slow down your tests.
  • Abstract-ability: keep logic isolated and tied into specific components, promoting re-usability, DRY and wide-ranging integration testing.

How will you develop your tests?

The tools we have discussed here are used with BDD and TDD. I personally think that TDD can be adopted for the majority of application projects, but is particularly suited for long term, protocol based projects, API services, critical package and infrastructure projects, as well as cutting-edge research projects. In agile development, perhaps opting for a particular method depending on your sprint goals will make sense for your team.

What would you prefer — a BDD approach of creating your testing suite after achieving your desired app behaviour? Or a TDD approach of integrating tests as your components are being developed, plugging in the final behavioural goal after your components are fully test-driven. Let me know!