How to Write Tests for React - Part 1 [Beginner's Guide]

Written by Kelvin9877 | Published 2020/06/06
Tech Story Tags: reactjs | testing | test-driven-development | front-end-development | tests-for-react | react-tests | write-tests-for-react | react

TLDR This article is intended to who just start to learn React and wonder how to write some simple tests with React applications. Jest and React Testing Library are the tools chain behind the test, they both are ship with create-react-app by default. The default test is written by Jest using Jest, and it put.test.js suffix after App component name as its.name. It is the default conventions suggested by team (link here) Jest is importing from a global setup file if you wonder.via the TL;DR App

Writing React Test with react recommended libraries — Jest & React Testing Library for complete beginners.
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.

First, let's start with the default example.

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 -
  • Where and how I can put my test files? - as you can see
    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).
  • Jest and React Testing Library are the tools chain behind the test, they both are ship with create-react-app by default.
// setupTests.js
// Jest is importing from a global setup file if you wonder
import '@testing-library/jest-dom/extend-expect';

Second, write a test for NavBar component.

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.
  • Jest and React Testing Library work together to make writing tests in React easy.
  • 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;

Third, write a signup form component test.

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):
  • An input field for name, which only allows string between 3 to 30 long.
  • An input field for email, which can check whether it is a valid email address.
  • An input field for the password, which can check its complexity (at least 1 number, 1 string in lower case, 1 string in upper case, 1 special character)
  • A submit button.
  • All 3 inputs above are required, can’t be empty.
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: 'marry123@test.com', password: '123WX&abcd' },
  { name: 'kent'.repeat(10), email: 'kent@testing.com', password: 'w%oRD123yes' },
  { name: 'Robert', email: 'robert_bell@example.com', 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.

Final words.

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.

Resources I have referenced to writing this article

Special Thanks to ooloo.io and Johannes Kettmann

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.

Written by Kelvin9877 | IT Pro, Web Dev
Published by HackerNoon on 2020/06/06