@Version : 4.5.0
@Build : d71caa7de
By using this site, you acknowledge that you have read and understand the Cookie Policy, Privacy Policy, and the Terms. Close

Patterns For Testable React Components

Posted Monday, December 30th, 2019

ReactJS
Patterns For Testable React Components

Decorated Components

Imagine the component below HelloName that renders Hello James when the value of name in prop is James. Since the name value is not owned by HelloName it is passed to it by the decorator High Order Component called WithName. This is common when using libraries that don't yet use React's context API.

import React from 'react';
import WithName from './WithName';

function HelloName({name}) {
	return (
		<div>
			Hello {name}
		</div>
	);
}

export default WithName(HelloName);

And the name decorator is like this.

import React from 'react';

export default function WithName(Component){
	class AddName extends React.Component {
		render(){
			return <Component name='James' />
		}
	}
 
    return AddName
}

The above relationship between HelloName and WithName is very common because it is used to achieve many things in Reactjs. If you have used Redux this is familiar as you will always use this kind of relationship to bind components to a global state.

To test HelloName as they are above, the test would look some thing like just rendering it.

import React from 'react';
import { render } from '@testing-library/react';
import HelloName from './HelloName';

test('Says Hello James', () => {
	const { getByText } = render(<HelloName />);
	const linkElement = getByText(/Hello James/i);
	expect(linkElement).toBeInTheDocument();
});

Now the problem is: - The goal of test is to give the component random possible states and props and check that the component works. Assume that you don't own the WithName component (which is very common). This means that you don't have easy access to the name variable to manipulate it to test different scenarios. This even gets more necessary when the value is used by HelloName to do some computation that would produce results instead of just rendering Hello {name}. For example, we need to test that it can say Hello James and Hello Mike in another scenario.

The solution: - The solution to this is pretty simple. Let's see. The first thing to do is just to export the undecorated HelloName component and make the decorated version the default.

import React from 'react';
import WithName from './WithName';

export function HelloName({name}) {
	return (
		<div>
			Hello {name}
		</div>
	);
}

export default WithName(HelloName);

And in the test, test the decorated version and then the undecorated version as many as you want.

import React from 'react';
import { render } from '@testing-library/react';
import {HelloName} from './HelloName';
import HelloNameDecorated from './HelloName';

test('Says Hello James', () => {
	const { getByText } = render(<HelloNameDecorated />);
	const element = getByText(/Hello James/i);
	expect(element).toBeInTheDocument();
});
test('Says Hello Mike', () => {
	const { getByText } = render(<HelloName name='Mike' />);
	const element = getByText(/Hello Mike/i);
	expect(element).toBeInTheDocument();
});
test('Says Hello Danstan', () => {
	const { getByText } = render(<HelloName name='Danstan' />);
	const element = getByText(/Hello Danstan/i);
	expect(element).toBeInTheDocument();
});
test('Says Hello Mary', () => {
	const { getByText } = render(<HelloName name='Mary' />);
	const element = getByText(/Hello Mary/i);
	expect(element).toBeInTheDocument();
});

This pattern has saved me a great deal when I was tasked to test a react app that was integrated heavily with Redux and Redux Form.

Testing Component Actions

Another pattern is when testing actions that happen within a component. Consider a component Form which renders an HTML form with one input for the value name bound to a state variable name. The form has a button that as onClick event bound to a function that is executed.

import React, { useState } from 'react';

export default function Form({ onSubmit }) {

	const [name, setName] = useState('')

	const handleSubmit = (e) => {
		// Do something with value of name
	}
	return (
		<form>
			<input onChange={(e) => setName(e.target.value)} name='name' value={name} />
			<button onClick={handleSubmit}>Submit</button>
		</form>
	)
}

Now we need to test that on render, when some text is typed into the input field followed by a click of the button, the function handleSubmit is called once.

The problem: - handleSubmit is defined inside the form component as is therefore inaccessible. To make this testable, remove the handleSubmit function from the component to props.

import React, { useState } from 'react';

export default function Form({ handleSubmit }) {

	const [name, setName] = useState('')
	return (
		<form onSubmit={() => handleSubmit({ name })}>
			<input onChange={(e) => setName(e.target.value)} name='name' value={name} />
			<button type='submit'>Submit</button>
		</form>
	)
}

To test this component now we just replace handleSubmit as its now in the props with a mock. See...

import React from 'react';
import Form from './Form';

import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() });

describe('Test Form Submit', () => {
	it('Test form Submit', () => {
		const mockCallBack = jest.fn();

		const form = Enzyme.shallow((<Form handleSubmit={mockCallBack}/>));
		form.find('input').simulate('change', {target: {value: 'James'}});
		form.simulate('submit');
		expect(mockCallBack.mock.calls.length).toEqual(1);
		expect(mockCallBack.mock.calls).toEqual([[{name: 'James'}]]);
	});
});

This is way of removing external actions from the component to the props gives power to manipulate the component to better test the actions that users will trigger while interacting with the component.

Components with complex state versions.

Consider you have the component below. Profile component which depends on variable user_id in localStorage thus has these states.

  • shows login page when there is no user_id in localStorage.
  • shows only when a user is logged
  • shows follow button if the user_id in localStorage is not the user_id in profile
  • shows edit button if the user_id in localStorage is the same as user_id in profile
import React from 'react';

export default function Profile() {

		const userId = localStorage.getItem('user_id');

		const user = { name: 'James Allen' } // From some API

		return (
				<div>

					{!userId && <div>Please Login First</div>}
					{
						userId &&
						<div>
							<h1>Profile</h1>

							<p>Name: {user.name}</p>

							{user.id === userId && <button>Edit</button>}

							{user.id !== userId && <button>Follow</button>}

						</div>
					}

				</div>
		)
}

Note, this is just an example. Do not use localstorage to represent login or not login. I dont usually think its the best way to do it even though it works and supper simple.

No to test the above component, we first have to ensure the data used to render the component is well abstracted from the component so that manipulating the component becomes easy.

First thing, Have a data owner component, a component with minimal side effects except make the data available. Possibly without render at all. In Redux, this would be a reducer then a provider with some HOC, or whatever they are called in MobX or Flux.

In my example solution, I have used the React Context to provide the data to the component. I have the ProfileDataContext/Provider.

import React from 'react';

export const ProfileDataContext = React.createContext({});

export const ProfileDataProvider = ProfileDataContext.Provider

export const ProfileDataDecorator = (ChildComponent) => {
	return function (){
		const data = {user: {name: 'Mike Allen', id: 2}, userId: 2} // user Received from some API, while user_id from localStorage
		return (
			<ProfileDataContext.Provider value={data}>
				<ChildComponent/>
			</ProfileDataContext.Provider>
		);
	}
}

And the component now looks like this.

import React, {useContext} from 'react';
import {ProfileDataContext, ProfileDataDecorator} from './ProfileDataProvider'

export function Profile(props) {

		const {user, userId} = useContext(ProfileDataContext)

		return (
				<div>

					{!userId && <div>Please Login First</div>}
					{
						userId &&
						<div>
							<h1>Profile</h1>

							<p>Name: {user.name}</p>

							{user.id === userId && <button>Edit</button>}

							{user.id !== userId && <button>Follow</button>}

						</div>
					}

				</div>
		)
}

export default ProfileDataDecorator(Profile)

Now in my tests, I can use the raw component and give it my own provider with my own data and boom! I can now test as many scenarios as I want no matter how complex the profile data is and the complexity of the render and even children in the Profile component. See how I test the four different states that represent the components versions.

import React from 'react';
import {Profile} from './Profile';
import { render, screen } from '@testing-library/react';
import { ProfileDataProvider } from './ProfileDataProvider';


describe('Profile Component', () => {
		test('Should show login only', () => {
				const withNoUser = {}
				const { getByText } = render(<ProfileDataProvider value={withNoUser}><Profile/></ProfileDataProvider>);
				const loginElement = getByText(/Please login first/i);
				expect(loginElement).toBeInTheDocument();
			});

		test('Should show Edit, no Follow', () => {
				const withUserSignedIn = {user: {name: 'Danstan Onyango', id: 2}, userId: 2}
				const { getByText } = render(<ProfileDataProvider value={withUserSignedIn}><Profile/></ProfileDataProvider>);
				const nameElement = getByText(/Name: Danstan Onyango/i);
				const EditElement = getByText(/Edit/i);
				expect(nameElement).toBeInTheDocument();
				expect(EditElement).toBeInTheDocument();
				expect(screen.queryByText(/Follow/i)).toBeNull()
			});
		test('Should show Follow, no Edit', () => {
				const withUserDifferentUserSignedIn = {user: {name: 'Danstan Onyango', id: 2}, userId: 3}
				const { getByText } = render(<ProfileDataProvider value={withUserDifferentUserSignedIn}><Profile/></ProfileDataProvider>);
				const nameElement = getByText(/Name: Danstan Onyango/i);
				const FollowElement = getByText(/Follow/i);
				expect(nameElement).toBeInTheDocument();
				expect(FollowElement).toBeInTheDocument();
				expect(screen.queryByText(/Edit/i)).toBeNull()
			});
	});

These methods make testing components very easy. The key points here are:

  • Make as pure components as you can.
  • The less the side effects components have, the easy they are to test precisely.
  • Abstract components actions to pure parents that you can mock easily.
  • Abstract data when you can.

I hope this has been helpful to you! If so just follow me on GitHub and Twitter and that will go a really long way. The entire code is on GitHub too 😄 Cheers!