What every developer should know about testing in React (Jest and React Testing Library)

Testing is an essential part of building reliable and secure React applications. As developers, it allows us to verify that React components are working correctly before implementing them in production. To perform such tests in React, it is important to use tools like Jest and React Testing Library.

Jest is a popular testing framework for JavaScript and React. It provides a wide range of functionalities for writing and executing tests, including assertions, date mocking, and mocks. In addition, Jest integrates seamlessly with React and can be used to test both components and functions.

React Testing Library is a testing library that focuses on testing the end-user functionality rather than the internal implementations of the components. React Testing Library is based on the idea that tests should simulate the end-user behavior and not the way the component is implemented. This means that tests written with React Testing Library are more realistic and useful for both developers and end-users.

In the following examples, you will discover how to write simple yet effective tests that will give you greater confidence in your code and help you avoid costly errors in the future. From a simple header component to a more complex login component with form validation.

We will start by looking at a simple React component without any tests:

const Greeter = (props) => <h1>Hey {props.name}!</h1> 

This component renders a header that greets the name it is passed. Now let’s imagine that we accidentally update the component like this:

const Greeter = (props) => <h1>Hey {props.nom}!</h1>

We used the incorrect prop nom instead of name. Without tests, we would never realize this subtle error. Let’s put some tests into practice:

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

test('Greeter renders correctly', () => {
  render(<Greeter name="Julian" />);
  expect(screen.getByText('Hey Julian!')).toBeInTheDocument();

This test renders the Greeter component with the name ‘Julian’ and verifies that the text ‘Hey Julian!’ is in the document. If we run this test with our component incorrectly updated, the test will obviously fail. This will alert us to the error and save us trouble in the future.

Now let’s take a look at a simple button component:

const Button = (props) => <button {...props} />

This component renders an HTML button. Let’s add some tests with React Testing Library:

import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import Button from './Button';

test('Button clicks', () => {
  const { getByText } = render(<Button />);
  const button = getByText('Click me');
  fireEvent.click(button); expect(button).toHaveTextContent('Click me');

This test renders the button, clicks it, and verifies that the button text still says “Click me”. This gives us confidence that the button is working correctly.

Let’s take another example with a counter component:

const Counter = () => {
  const [count, setCount] = useState(0);

  return <div>{count}<button onClick={() => setCount(count + 1)}>+</button></div>

Here are some tests for this component:

test('Counter starts at 0', () => {
  const { getByText } = render(<Counter />);

test('Clicking + button increments counter', () => {
  const { getByText } = render(<Counter />);
  const button = getByText('+');

The first test verifies that the counter starts at 0. The second test clicks the + button and verifies that the counter increases to 1.

Let’s take a closer look at the final example, a slightly more complex login component with form validation:

const Login = () => {
  const [form, setForm] = useState({ email: '', password: '' });

  function handleChange(e) {
    setForm({ ...form, [e.target.name]: e.target.value });

  function handleSubmit(e) {
    if (form.email && form.password) {
      console.log('Login successful!');
    } else {
      console.log('Please enter email and password!');

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" onChange={handleChange} />
      <input name="password" onChange={handleChange} />
      <button type="submit">Submit</button>

Alright, let’s implement some tests for our code:

test('Form initial state', () => {
  const { getByPlaceholderText } = render(<Login />);

test('Form values update on change', () => {
  const { getByPlaceholderText } = render(<Login />);
  fireEvent.change(getByPlaceholderText('Email'), { target: { value: 'test@example.com' } });

test('Form submits with valid data', () => {
  const { getByPlaceholderText, getByText } = render(<Login />);
  fireEvent.change(getByPlaceholderText('Email'), { target: { value: 'test@example.com' } });
  fireEvent.change(getByPlaceholderText('Password'), { target: { value: 'password' } });
  expect(console.log).toHaveBeenCalledWith('Login successful!');

In this case, the tests executed verify the initial state of the form, that the input values are updated correctly, and that the form is submitted correctly with valid data. With this, our Login component will work as expected, in a clean and error-free way.

Tests are essential for creating production applications free of annoying errors and avoiding unwanted headaches. The more complex our application, the more important it is to have a solid suite of tests. We should aim to have wide test coverage to cover as much of our application as possible, even if we don’t reach 100%, we should ensure that the most important elements do not present any issues.

Although it may seem tedious to have to write extra code to test our application, this can prevent us from spending a lot of time searching for errors and trying to fix them.

That your code be as perfect as a summer breeze! Best wishes!

Posted in Blog, DesignTags:
Write a comment