Writing React Test with react recommended libraries — Jest & React Testing Library for complete beginners.
From https://reactjs.org/docs/test-utils.html#overview
This article is intended to who just start to learn React and wonder how to write some simple tests with their React applications. And just like most of the people start to create React app using
create-react-app
, I would start with it as well.Default Dependencies with create-react-app (2020/05/22)
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1"
}
There is one test already written to help you to start.
// src/App.test.js
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
const { getByText } = render(<App />); //render is from @testing-library/react
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument(); //expect assertion is from Jest
});
If you run the command
$ yarn test App
, you will see a similar result as following:With default create-react-app setting, you can start to write a test without install or configure anything.
From the example above, we should learn -
App.test.js
file is put next to App.js
file in the same folder, and it put .test.js
suffix after App component name as its filename. It is the default conventions suggested by create-react-app
team (link here).// setupTests.js
// Jest is importing from a global setup file if you wonder
import '@testing-library/jest-dom/extend-expect';
I am creating a NavBar component that contains links and logo in it.
First, I would start writing test without writing the actual component (Test Drive Development).
import React from 'react';
// screen newer way to utilize query in 2020
import { render, screen } from '@testing-library/react';
import NavBar from './navBar'; // component to test
test('render about link', () => {
render(<NavBar />);
expect(screen.getByText(/about/)).toBeInTheDocument();
})
The test will fail first since I didn't write any code in navBar.js component yet.
With code below in navBar.js, the test should pass now.
// navBar.js
import React from 'react';
const NavBar = () => (
<div className="navbar">
<a href="#">
about
</a>
</div>
);
export default NavBar;
For now, you should learn:
expect( ... ).toBeInTheDocument()
assertion is from Jest.render(<NavBar />);
and screen.getByText(/about/)
is from Testing Library.screen.getByText(/about/)
use "getByText" instead of select by class name is because React Testing Library adapting the mindset of focus on User experiences over implementation detail. To learn more to expand and alter the test, you can check out following resources:
Now let’s expand the test and component to make it more real -
// navBar.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import NavBar from './navBar';
// include as many test cases as you want here
const links = [
{ text: 'Home', location: "/" },
{ text: 'Contact', location: "/contact" },
{ text: 'About', location: "/about" },
{ text: 'Search', location: "/search" },
];
// I use test.each to iterate the test cases above
test.each(links)(
"Check if Nav Bar have %s link.",
(link) => {
render(<NavBar />);
//Ensure the text is in the dom, will throw error it can't find
const linkDom = screen.getByText(link.text);
//use jest assertion to verify the link property
expect(linkDom).toHaveAttribute("href", link.location);
}
);
test('Check if have logo and link to home page', () => {
render(<NavBar />);
// get by TestId define in the navBar
const logoDom = screen.getByTestId(/company-logo/);
// check the link location
expect(logoDom).toHaveAttribute("href", "/");
//check the logo image
expect(screen.getByAltText(/Company Logo/)).toBeInTheDocument();
});
This is what a NavBar component usually look like (maybe need add some styles).
// navBar.js
import React from 'react';
const NavBar = () => (
<div className="navbar">
<a href="/" data-testid="company-logo">
<img src="/logo.png" alt="Company Logo" />
</a>
<ul>
<li>
<a href="/"> Home </a>
</li>
<li>
<a href="/about"> About </a>
</li>
<li>
<a href="/contact"> Contact </a>
</li>
<li>
<a href="/search"> Search </a>
</li>
</ul>
</div>
);
export default NavBar;
After writing a test for static content, let's write a test for more dynamic content - a signup form.
First, let's think in TDD way - what we need in this signup form (no matter how it look):
Now, let's write the test.
/* Prepare some test cases, ensure 90% edge cases are covered.
You can always change your test cases to fit your standard
*/
const entries = [
{ name: 'John', email: 'john_doe@yahoo', password: 'helloworld' },
{ name: 'Jo', email: 'jo.msn.com', password: 'pa$$W0rd' },
{ name: '', email: '[email protected]', password: '123WX&abcd' },
{ name: 'kent'.repeat(10), email: '[email protected]', password: 'w%oRD123yes' },
{ name: 'Robert', email: '[email protected]', password: 'r&bsEc234E' },
]
Next, build up the skull of the test.
// signupForm.test.js
// this mostly a input validate test
describe('Input validate', () => {
/*
I use test.each to iterate every case again
I need use 'async' here because wait for
validation is await function
*/
test.each(entries)('test with %s entry', async (entry) => {
...
})
})
Now, let building the block inside the test.
// signupForm.test.js
...
test.each(entries)('test with %s entry', async (entry) => {
//render the component first (it will clean up for every iteration
render(<SignupForm />);
/* grab all the input elements.
I use 2 queries here because sometimes you can choose
how your UI look (with or without Label text) without
breaking the tests
*/
const nameInput = screen.queryByLabelText(/name/i)
|| screen.queryByPlaceholderText(/name/i);
const emailInput = screen.getByLabelText(/email/i)
|| screen.queryByPlaceholderText(/email/i);
const passwordInput = screen.getByLabelText(/password/i)
|| screen.queryByPlaceholderText(/password/i);
/* use fireEvent.change and fireEvent.blur
to change name input value
and trigger the validation
*/
fireEvent.change(nameInput, { target: { value: entry.name } });
fireEvent.blur(nameInput);
/* first if-statement to check whether the name is input.
second if-statement to check whether the name is valid.
'checkName' is a utility function you can define by yourself.
I use console.log here to show what is being checked.
*/
if (entry.name.length === 0) {
expect(await screen.findByText(/name is required/i)).not.toBeNull();
console.log('name is required.');
}
else if (!checkName(entry.name)) {
// if the name is invalid, error msg will showup somewhere
expect(await screen.findByText(/invalid name/i)).not.toBeNull();
console.log(entry.name + ' is invalid name.');
};
// With a similar structure, you can continue building the rest of the test.
...
/* Remember to add this line at the end of your test to
avoid act wrapping warning.
More detail please checkout Kent C.Dodds's post:
(He is the creator of Testing Library) <https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning>
*/
await act(() => Promise.resolve());
})
...
For complete Testing code, please find them here.
Ok, now the test is done (maybe we will come back to tweak a bit, but let's move on for now), let's write the component.
// signupForm.js
import React from 'react';
/*
I borrow the sample code from formik library with some adjustments
<https://jaredpalmer.com/formik/docs/overview#the-gist>
*/
import { Formik } from 'formik';
/*
For validation check, I wrote 3 custom functions.
(I use the same functions in test)
*/
import {
checkName,
checkEmail,
checkPassword,
} from '../utilities/check';
const SignupForm = () => (
<div>
<h1>Anywhere in your app!</h1>
<Formik
initialValues={{ name: '', email: '', password: '' }}
validate={values => {
const errors = {};
if (!values.name) {
errors.name = 'Name is Required'
} else if (!checkName(values.name)) {
errors.name = `invalid name`;
}
if (!values.email) {
errors.email = 'Email is Required';
}
else if (!checkEmail(values.email)) {
errors.email = 'Invalid email address';
}
if (!values.password) {
errors.password = 'Password is Required';
} else if (!checkPassword(values.password)) {
errors.password = 'Password is too simple';
}
return errors;
}}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
setSubmitting(false);
}, 400);
}}
>
{({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
isSubmitting,
/* and other goodies */
}) => (
<form onSubmit={handleSubmit}>
<label>
Name:
<input
type="text"
name="name"
placeholder="Enter your name here"
onChange={handleChange}
onBlur={handleBlur}
value={values.name}
/>
</label>
<p style={{ 'color': 'red' }}>
{errors.name && touched.name && errors.name}
</p>
<label>
Email:
<input
type="email"
name="email"
placeholder="Your Email Address"
onChange={handleChange}
onBlur={handleBlur}
value={values.email}
/>
</label>
<p style={{ 'color': 'red' }}>
{errors.email && touched.email && errors.email}
</p>
<label>
Password:
<input
type="password"
name="password"
placeholder="password here"
onChange={handleChange}
onBlur={handleBlur}
value={values.password}
/>
</label>
<p style={{ 'color': 'red' }}>
{errors.password && touched.password && errors.password}
</p>
<button type="submit" disabled={isSubmitting}>
Submit
</button>
</form>
)}
</Formik>
</div>
);
export default SignupForm;
And the form will look similar like below (no much style, but good enough for our purpose), And with wrong input, the error message will show below the input:
If you finished the test above, now the test should all pass, run
yarn test --verbose
, with the verbose option and console.log message, you can see how each case is being tested and which one is a good case and which one is not.For more testing code examples and different cases, please check out my repo here.
It is difficult for a beginner to learn all of it once so just slow down if it's overwhelming. It took me at least an entire week to learn the basics, and this is just the beginning of writing tests for React applications.
It is a hard topic to grasp, but I believe it is worthy to spend some time on it if you want to become a Front-end developer.
And the good news is, you have a good start, you should now know how to leverage Jest and React Testing Library to write a test around your react components, and you can start to explore other libraries and solutions out there with this good foundation.
I am planning to write another article to cover more advance examples if I got positive feedback on this article, Thanks again for your time.
For someone who wants to become a job-ready FrontEnd developer, I would recommend trying a course from ooloo.io. It introduces concepts such as - Creating pixel-perfect design, Planning and implementing a complex UI component, Debugging inside IDE, and Writing integration tests, which are not necessary would see from most of the online tutorials or courses. And Yes, I got a lot of inspiration from this course which helped me write up this article eventually.