Posted Monday, December 30th, 2019
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.
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 has 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.
Consider you have the component below. Profile component which depends on variable user_id
in localStorage
thus has these states.
user_id
in localStorage
.user_id
in localStorage
is not the user_id
in profile
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.
Now 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:
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!
Thank you for finding time to read my post. I hope you found this helpful and it was insightful to you. I enjoy creating content like this for knowledge sharing, my own mastery and reference.
If you want to contribute, you can do any or all of the following 😉. It will go along way! Thanks again and Cheers!