Implementing Essential UI Component Tests for Frontend Development with React Testing Library

  • testinglibrary
    testinglibrary
  • jest
    jest
  • typescript
    typescript
  • nextjs
    nextjs
Published on 2023/09/20

Introduction

In automated frontend testing, UI component tests are probably something you always encounter.

In particular, components that are shared and used across many screens are often subject to frequent changes.

Manually checking every screen each time to ensure that existing pages are not affected when something is changed is a huge amount of work.

There are many situations where it’s better to add automated tests for UI components to ensure that the DOM is rendered as before and that event behavior such as onClick and onChange has not changed.

In React, React Testing Library is commonly used as the de facto tool for this.

I’ll introduce automated testing of UI components by walking through how to set up React Testing Library and how to use it with simple examples.

What is React Testing Library?

React Testing Library is a library for testing React components.

It focuses on testing from the user’s perspective, allowing you to write tests that are close to real usage scenarios without depending on implementation details of the components.

The goal of this library is to make tests less sensitive to changes in application code so that they are easier to maintain and closer to production behavior.

More concretely, React Testing Library has the following characteristics:

  • Focus on user interaction:
    You can write tests as if a user is operating the application, verifying that the UI responds as expected through button clicks and input operations.

  • Independent of implementation details:
    Tests are based on elements and text rendered in the DOM, so they are less likely to break even if the implementation changes slightly.

  • Accessibility-friendly:
    It provides queries such as getByRole, making it easier to write tests that take accessibility into account. This allows you to obtain DOM elements in a way that is closer to how real users interact with them.

  • Easy to combine with Jest:
    By using it together with testing frameworks such as Jest, you can improve the overall test coverage of your React application.

https://testing-library.com/docs/

Goal of this article

We’ll refer to the official React Testing Library documentation to set it up and write some simple test code.

https://testing-library.com/docs/react-testing-library/intro/

https://www.robinwieruch.de/react-testing-library/

In the end, we’ll implement tests for interactive UI components that receive user actions (events) in a component.

search.test.tsx
describe('Search', () => {
  it('renders Search component', () => {
    render(<Search />)

    fireEvent.change(screen.getByRole('textbox'), {
      target: { value: 'Next.js' },
    })

    waitFor(() =>
      expect(
        screen.getByText(/Searches for JavaScript/)
      ).toBeInTheDocument()
    )
  })
})

Preparation for introducing React Testing Library

To introduce React Testing Library, we first need an environment where React components can run.

You could install each library one by one, but to get as close as possible to a real-world development environment, we’ll set up Next.js.

Create an appropriate folder and set it up with create-next-app.

mkdir react-testing-library-ui-testing
cd react-testing-library-ui-testing
npx create-next-app@14.2.2 .

Image from Gyazo

Once setup is complete, make sure that Next.js can be started locally.

npm run dev

Setting up Jest

React Testing Library itself can work not only with Jest but also with Vitest and others, but here we’ll use Jest.

Install the required packages all at once.

npm install --save-dev jest ts-jest ts-node @types/jest jest-environment-jsdom

After installation, we’ll check that Jest runs correctly.

Jest configuration

First, create a jest.config.ts file and configure it.

jest.config.ts
import type {Config} from 'jest';

const config: Config = {
  clearMocks: true,
  coverageProvider: "v8",
  preset: "ts-jest",
  testEnvironment: "jest-environment-jsdom",
};

export default config;

Add a script to package.json to run Jest.

package.json
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
-     "lint": "next lint"
+     "lint": "next lint",
+     "test": "jest"
  },
  "dependencies": {

Sample test code

We’ll write a simple function and its test code.

Create a utils folder and add the following file.

sum.ts
export function sum(a: number, b: number) {
  return a + b;
}

Create the test code in the same folder.

sum.test.ts
import { sum } from './sum';

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3)
})

Checking Jest works

Run the following command to execute the test.

npm test

Image from Gyazo

Once you’ve confirmed it works, you can delete the two files above.

Introducing React Testing Library

Now we finally get to the main topic: React Testing Library.

First, install the required packages.

npm install --save-dev @testing-library/react @testing-library/dom @testing-library/jest-dom

Configuration so Jest can handle JSX

We’ll specify how to transform the files under test using a specific transformer tool. Simply put, this defines “how Jest should process files.”

jest.config.ts
import type {Config} from 'jest';

const config: Config = {

  clearMocks: true,
  coverageProvider: "v8",
  preset: "ts-jest",
  testEnvironment: "jest-environment-jsdom",
+ transform: {
+     '^.+\\.(ts|tsx)$': ['ts-jest', { tsconfig: { jsx: 'react-jsx'}}],
+ },
  setupFilesAfterEnv: ['./jest.setup.ts'],
};

export default config;
  • Process .ts and .tsx files
    Jest converts TypeScript files into executable JavaScript.
    This allows you to directly test test code and components written in TypeScript.

  • Properly interpret JSX syntax
    When testing React components, JSX syntax (e.g. <div>Hello</div>) is correctly interpreted and transformed into executable JavaScript.

  • Perform TypeScript type checking and transpilation at the same time
    ts-jest can also perform TypeScript type checking.
    If there are type errors during test execution, they will be reported as errors.

Creating the component to test

Create a components folder and add an App component.

app.tsx
export function App() {
  return <div>Hello Next.js</div>;
}

Test code for the component

Now we’ll use the packages we installed earlier to write test code for the component.

Before writing the test, let’s first render the component and check that it renders correctly.

app.test.tsx
import { render, screen } from '@testing-library/react';
import { App } from './app'

describe('App', () => {
  it('renders App component', () => {
    render(<App />)

    screen.debug()
  })
})

After rendering the component, we use the screen.debug() method.
This is a handy method when writing tests, as it lets you inspect what was rendered.

https://testing-library.com/docs/dom-testing-library/api-debugging/#screendebug

If you’re testing a component you implemented yourself, you probably already know what will be rendered, so you may not need it.

However, you might use it when adding tests to components written by someone else.

When you run this test, you’ll see output like the following:

Image from Gyazo

You can confirm that Hello Next.js wrapped in a div tag is being rendered.

Now that we’ve confirmed that the text is rendered, let’s write an actual test.

app.test.tsx
import { render, screen } from '@testing-library/react'
import { App } from './app'
+ import '@testing-library/jest-dom'

describe('App', () => {
  it('renders App component', () => {
    render(<App />)

-     screen.debug()
+     expect(screen.getByText('Hello Next.js')).toBeInTheDocument()
  })
})

This is a very simple test that checks that the text Hello Next.js is rendered.

Since we’re using toBeInTheDocument(), we also import @testing-library/jest-dom.

Run the test again and you’ll see that it passes.

Image from Gyazo

Tests involving events

Next, we’ll create a component where the user inputs text and test whether the entered content is rendered correctly.

Create a new Search component.

search.tsx
import { useState } from "react";

export function Search() {
  const [search, setSearch] = useState('');

  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
    setSearch(event.target.value);
  }

  return (
    <div>
      <Input value={search} onChange={handleChange}>
        Search:
      </Input>

      <p>Searches for {search ? search : '...'}</p>
    </div>
  );
}

function Input({
  value,
  onChange,
  children
}: {
  value: string,
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void,
  children: React.ReactNode
}) {
  return (
    <div>
      <label htmlFor="search">{children}</label>
      <input
        id="search"
        type="text"
        value={value}
        onChange={onChange}
      />
    </div>
  )
}

We use useState to manage the entered value and display it.

We’ll write the test code, but first let’s render it once and inspect the DOM structure.

search.test.tsx
import { render, screen } from '@testing-library/react'
import { Search } from './search'
import '@testing-library/jest-dom'
describe('Search', () => {
  it('renders Search component', () => {
    render(<Search />)

    screen.debug()
  })
})

Image from Gyazo

You can see that the input tag for entering text and the p tag that displays the entered content are rendered.
Since nothing has been entered yet, ... is displayed after Searches for.

As you may have already noticed, things like useState and the handleChange function that appear in the actual code do not appear in the rendered result.

Also, although we split the implementation into two components, at render time they appear as a single DOM structure.

React Testing Library is used to interact with React components as a human would.

Humans see the HTML rendered from React components, so they see this HTML structure as the output, not the two separate React components.

Now that we’ve confirmed the rendered content of the component, let’s actually input some text.

We’ll use React Testing Library’s fireEvent to simulate text input and button clicks.

search.test.tsx
- import { render, screen } from '@testing-library/react'
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { Search } from './search'
import '@testing-library/jest-dom'
describe('Search', () => {
  it('renders Search component', () => {
    render(<Search />)

+     fireEvent.change(screen.getByRole('textbox'), {
+       target: { value: 'Next.js' },
+     })

+     waitFor(() =>
      screen.debug()
+     )
  })
})

The fireEvent function takes an element (here, the input field with the textbox role) and an event (here, an event with the value "Next.js").

After input, we render again and check that the entered content is reflected.

Here is the test result:

Image from Gyazo

You can see that the text Next.js is reflected in the value of the input tag and displayed after Search for.

We added waitFor before displaying the rendered result.

waitFor is necessary because after using fireEvent.change to change the value of the search box, the re-render may happen asynchronously.

Specifically, an element like screen.getByText(/Searches for JavaScript/) may not be rendered immediately after fireEvent, but may be updated slightly later.

Now that we’ve visually confirmed that the component renders correctly, let’s make it more like a real test:
we’ll test that when the text Next.js is entered, Search for Next.js is displayed.

search.test.tsx
describe('Search', () => {
  it('renders Search component', () => {
    render(<Search />)

    fireEvent.change(screen.getByRole('textbox'), {
      target: { value: 'Next.js' },
    })

    waitFor(() =>
-       screen.debug()
+       expect(
+         screen.getByText(/Searches for JavaScript/)
+       ).toBeInTheDocument()
+     )
  })
})

In this way, React Testing Library allows you to test the behavior of components when users interact with them.

Creating a setup file

So far, whenever we used toBeInTheDocument() in our test code, we imported @testing-library/jest-dom.
When writing component tests, you’ll very often use various methods from @testing-library/jest-dom.

  1. toBeInTheDocument()
    Checks whether an element exists in the DOM.
expect(screen.getByText('Hello')).toBeInTheDocument()
  1. toHaveTextContent()
    Checks whether an element contains specific text.
expect(screen.getByRole('heading')).toHaveTextContent('Welcome');
  1. toBeVisible()
    Checks whether an element is visible to the user (i.e., not hidden by CSS such as display: none or visibility: hidden).
expect(screen.getByText('Click me')).toBeVisible();
  1. toHaveAttribute()
    Checks whether an element has a specific attribute or whether an attribute has a specific value.
expect(screen.getByRole('button')).toHaveAttribute('disabled');
expect(screen.getByRole('button')).toHaveAttribute('type', 'submit');
  1. toHaveClass()
    Checks whether an element has a specific class.
expect(screen.getByTestId('my-element')).toHaveClass('active');
  1. toHaveStyle()
    Checks whether a specific style is applied to an element.
expect(screen.getByText('Hello')).toHaveStyle('color: red');
  1. toContainElement()
    Checks whether a given parent element contains the specified element.
expect(container).toContainElement(screen.getByText('Child Element'));
  1. toBeEmptyDOMElement()
    Checks whether an element is empty (has no child elements or text).
expect(screen.getByTestId('empty-div')).toBeEmptyDOMElement();
  1. toHaveFocus()
    Checks whether an element currently has focus.
expect(screen.getByRole('textbox')).toHaveFocus();
  1. toBeDisabled() / toBeEnabled()
    Checks whether an element is disabled (has the disabled attribute).
expect(screen.getByRole('button')).toBeDisabled();
  1. toHaveValue()
    Checks whether elements such as input or textarea have a specific value.
expect(screen.getByRole('textbox')).toHaveValue('Next.js');

Because of this, when writing test code you’ll very often need to import @testing-library/jest-dom.

So I’ll show you how to create a Jest setup file that always imports @testing-library/jest-dom before tests run.

Create a jest.setup.ts file in the project root and add the import.

jest.setup.ts
import '@testing-library/jest-dom'

After creating the setup file, add one line to jest.config.ts.

jest.config.ts
import type {Config} from 'jest';

const config: Config = {

  clearMocks: true,
  coverageProvider: "v8",
  preset: "ts-jest",
  testEnvironment: "jest-environment-jsdom",
  transform: {
      '^.+\\.(ts|tsx)$': ['ts-jest', { tsconfig: { jsx: 'react-jsx'}}],
  },
+   setupFilesAfterEnv: ['./jest.setup.ts'],
};

export default config;

That completes the configuration.

Finally, as a sanity check, remove the line that imports @testing-library/jest-dom from the test code and run the tests.

app.tsx
import { render, screen } from '@testing-library/react'
import { App } from './app'
- import '@testing-library/jest-dom'
describe('App', () => {
  it('renders App component', () => {
    render(<App />)

    expect(screen.getByText('Hello Next.js')).toBeInTheDocument();
  })
})

Conclusion

In this article, we introduced React Testing Library and implemented UI component tests using actual components.

We also implemented tests for behavior that responds to user actions (events), allowing us to verify that components provide their functionality correctly.

Shared components are often used across many screens, and even small changes can have a large impact.

I hope implementing UI component tests will provide a foundation for stable maintenance and operation.

Xでシェア
Facebookでシェア
LinkedInでシェア

Questions about this article 📝

If you have any questions or feedback about the content, please feel free to contact us.
Go to inquiry form