Тестирование React приложений с помощью Jest и React Testing Library

Тестирование - важная практика в разработке программного обеспечения. Оно помогает создавать надежное и качественное программное обеспечение и повышает уверенность команды в коде, делая приложение более гибким и подверженным меньшему количеству ошибок при внедрении или изменении функций.

Высокоэффективные команды делают тестирование основной практикой в своей повседневной работе, и никакие функции не выпускаются до того, как будут внедрены автоматизированные тесты. Некоторые разработчики даже пишут тесты перед написанием функций, следуя процессу, называемому TDD (разработка через тестирование).

В этой статье мы протестируем приложения React с помощью Jest и React Testing Library, популярного сочетания среды тестирования JavaScript и утилиты React для тестирования компонентов. Это также официальная рекомендация по документации React.

Что такое тестирование?

Тестирование - это процесс автоматизации утверждений между результатами, которые производит код, и ожидаемыми результатами.

При тестировании приложений React наши утверждения определяются тем, как приложение отображает и реагирует на действия пользователя.

Есть много разных типов парадигм и философий тестирования. В этой статье основное внимание будет уделено созданию модульных и компонентных тестов (или интеграционных тестов).

Введение в библиотеку тестирования Jest и React

Что такое Jest?

Jest - это среда тестирования JavaScript, которая позволяет разработчикам запускать тесты на коде JavaScript и TypeScript и хорошо интегрируется с React.

Это фреймворк, разработанный с учетом простоты и предлагающий мощный и элегантный API для создания изолированных тестов, сравнения снимков, фиксации, покрытия тестами и многого другого.

Что такое React Testing Library?

Библиотека тестирования React - это утилита для тестирования JavaScript, созданная специально для тестирования компонентов React. Он имитирует взаимодействие пользователя с изолированными компонентами и утверждает их выходные данные, чтобы гарантировать правильное поведение пользовательского интерфейса.

Настройка вашей тестовой среды

Начнем с установки необходимых библиотек и настройки проекта. Самый простой способ запустить и запустить приложение React - использовать Create React App, которое поставляется с уже предустановленным Jest.

Сначала создайте приложение React:

npx create-react-app react-jest-tutorial

Теперь установите React Testing Library:

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

Наконец, установите дополнительные библиотеки:

npm install axios

Разработка React приложения для тестирования

Далее мы создаем минимальное приложение, которое будет отображать пользователей из API. Поскольку мы фокусируемся только на интерфейсе, мы будем использовать пользовательский API JSONPlaceHolder. Это приложение создано исключительно для построения тестов.

Замените содержимое файла App.js следующим:

import { useEffect, useState } from 'react';
import axios from 'axios';
import { formatUserName } from './utils';
import './App.css';

function App() {
 const [users, setUsers] = useState([]);

 // Load the data from the server
 useEffect(() => {
   let mounted = true;

   const getUsers = async () => {
     const response = await axios.get('https://jsonplaceholder.typicode.com/users');
     if (mounted) {
       setUsers(response.data);
     }
   };

   getUsers();

   return () => { mounted = false; }
 }, []);

 return (
   <div className="App">
     <div>Users:</div>
     {users.length ? (
       <ul data-testid="user-list">
         {users.map(user => (
           <li key={user.id} className="user" data-testid="user-item">
             <span>{user.name}</span> (<span>{formatUserName(user.username)}</span>)
           </li>
         ))}
       </ul>
     ) : (
       <div>Loading users...</div>
     )}
   </div>
 );
}

export default App;

Затем создайте файл с именем utils.js в папке src и напишите следующую функцию:

export function formatUserName(username) {
 return '@' + username;
}

Теперь вы можете запустить приложение с помощью этой команды:

npm start

После этого вы должны увидеть на экране следующее:

Базовое приложение, в котором перечислены пользователи из API с соответствующими именами пользователей

Создание модульного теста

Модульные тесты тестируют отдельные модули или компоненты программного обеспечения по отдельности. Единицей может быть функция, процедура, метод, модуль или объект, и цель тестирования - определить, выдает ли модуль ожидаемые результаты для заданного входного начения.

Тестовый модуль включает в себя серию методов, предоставляемых Jest для описания структуры тестов. Мы можем использовать такие методы, как describe или test, следующим образом:

describe('my function or component', () => {
 test('does the following', () => {
   // Magic happens here
 });
});

Блок describe - это набор тестов, а test - это тестовый кейс. В наборе тестов может быть несколько тестовых кейсов, и тестовый кейс не обязательно должен быть в тестовом наборе, хотя это обычная практика.

Внутри тестового примера мы пишем утверждения (например, expect в Jest), которые проверяют успешность (зеленый) или ошибочность (красный) утверждения. В каждом тестовом примере может быть несколько утверждений.

Вот несколько тривиальный пример утверждения, которое оказывается успешным:

describe('true is truthy and false is falsy', () => {
 test('true is truthy', () => {
   expect(true).toBe(true);
 });

 test('false is falsy', () => {
   expect(false).toBe(false);
 });
});

Затем давайте напишем наш первый тестовый пример, ориентированный на функцию formatUserName из модуля utils.

Нам нужно создать новый файл: utils.test.js. Обратите внимание, что все тестовые файлы используют шаблон {file}.test.js, где {file} - имя файла модуля для тестирования.

Наша функция, о которой идет речь, принимает строку в качестве входных данных и выводит ту же строку, добавляя @ в ее начале. Наша тестовая функция может утверждать, что для данной строки, например, «jc», функция выведет «@jc».

Вот код тестового файла:

import { formatUserName } from "./utils";

describe('utils', () => {
 test('formatUserName adds @ at the beginning of the username', () => {
   expect(formatUserName('jc')).toBe('@jc');
 });
});

Мы подробно описываем, что это за модуль и для чего предназначен тестовый пример, чтобы в случае сбоя мы получили четкое представление о том, что могло пойти не так.

Теперь, когда наш первый тест готов, мы можем запустить его и посмотреть, какие результаты. CRA позволяет нам легко запускать все тесты с помощью простой команды npm.

npm run test

А пока давайте сосредоточимся на выполнении одного теста, используя команду:

npm run test -- -t 'utils'

Мы делаем это, потому что у нас есть другие тесты, уже созданные CRA, которые нам нужно пока игнорировать.

Если все прошло хорошо, вы должны увидеть аналогичный результат:

Тестирование React приложений

Обратите внимание, что один тест пропущен (мы так и хотели), и что один тест прошел успешно.

Но что будет, если что-то пойдет не так? Давайте добавим новый тест в набор тестов utils, чтобы выяснить это.

test('formatUserName does not add @ when it is already provided', () => {
   expect(formatUserName('@jc')).toBe('@jc');
 });

Сейчас ситуация иная; если имя пользователя уже содержит символ @ в начале строки, мы ожидаем, что функция вернет имя пользователя, как указано, без добавления второго символа.

Давай запустим и посмотрим, что получится:

Тестирование React приложений

Как и предполагалось, тест не прошел, и мы получаем информацию о том, какой из ожидаемых вызовов завершился неудачно, ожидаемое значение и фактический результат. Поскольку мы обнаружили проблему с нашей исходной функцией, мы можем ее исправить.

export function formatUserName(username) {
 return username.startsWith('@') ? username : '@' + username;
}

И еще раз запустим наши тесты:

Тестирование React приложений

Мы написали два тестовых примера для нашего приложения, обнаружили ошибку благодаря написанию этих тестовых примеров и смогли исправить ее перед выпуском.

Тестирование компонентов с помощью Jest

Компоненты тестирования мало чем отличаются от функций тестирования. Идея и концепции совпадают. Разница в том, как мы пишем утверждения.

Мы протестируем наш компонент приложения, построив несколько тестовых примеров, и в каждом тестовом примере мы представим разные кейсы, которые мы можем сделать для проверки компонентов React.

Наш первый тест будет элементарным. Он будет только проверять рендеринг компонентов.

Перейдите к файлу App.test.js (автоматически сгенерированному CRA) и замените его содержимое на:

import { render } from '@testing-library/react';
import App from './App';

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

Как и раньше, у нас есть description-block и test-block, но на этот раз мы используем функцию рендеринга mount и визуализируем отдельный компонент изолированно. Этот тест завершится неудачно только в том случае, если есть ошибка компиляции или ошибка в функциональном компоненте, которая препятствует его визуализации. Несмотря на то, что он рабочий, это не полный тест, потому что он не выполняет никаких утверждений.

Чтобы исправить это, мы могли бы сопоставить содержимое в компоненте и посмотреть, есть ли оно, например:

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

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


   expect(screen.getByText('Users:')).toBeInTheDocument();
 });
})

Наш новый тест не только проверяет, что компонент был срендерен, но также ищет элемент, присутствующий в DOM, с текстом «Users:», что в нашем случае так и есть, и, таким образом, тест прошел успешно.

Экран объектов необходим в библиотеке тестирования React, поскольку он предоставляет вспомогательные методы для взаимодействия с компонентами и их элементами.

Ожидание асинхронных операций

Далее попробуем убедиться, что список пользователей отображается с элементами после завершения работы API. Для этого мы можем написать следующий тестовый пример:

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

describe('App component', () => {
 test('it displays a list of users', async () => {
   render(<App />);


   expect(screen.getByTestId('user-list')).toBeInTheDocument();
 });
});

Однако, когда мы запускаем тесты, он выдает следующее сообщение:

Тестирование React приложений

Причина сбоя проста: асинхронная операция (fetch) все еще не завершена, когда мы обращаемся к screen, поэтому вместо списка пользователей отображается сообщение «*Loading users…*».

Решение - ожидать завершения вызова:

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

describe('App component', () => {
 test('it displays a list of users', async () => {
   render(<App />);


   const userList = await waitFor(() => screen.getByTestId('user-list'));
   expect(userList).toBeInTheDocument();
 });
});

И теперь тесты проходят успешно.

Имитация (Mock) с помощью React и Jest

Наш следующий шаг - проверить, как компонент будет реагировать на данные, собранные из API. Но как мы можем проверить данные, если мы не уверены, каким будет ответ API? Решение этой проблемы - Mocking (имитация).

Цель имитации - изолировать тестируемый код от внешних зависимостей, таких как вызовы API. Это достигается заменой зависимостей управляемыми объектами, которые моделируют эти зависимости.

Mocking - это трехэтапный процесс:

  1. Импорт зависимостей
  import axios from 'axios';
  1. Имитация зависимостей
  jest.mock('axios');
  1. Подделка выходов функции
  axios.get.mockResolvedValue({ data: fakeUsers });

Давайте посмотрим, как они работают:

import axios from 'axios';
import { render, screen, waitFor } from '@testing-library/react';
import App from './App';

jest.mock('axios');

const fakeUsers = [{
  "id": 1,
  "name": "Test User 1",
  "username": "testuser1",
 }, {
  "id": 2,
  "name": "Test User 2",
  "username": "testuser2",
 }];

describe('App component', () => {

 test('it displays a row for each user', async () => {
   axios.get.mockResolvedValue({ data: fakeUsers });
   render(<App />);


   const userList = await waitFor(() => screen.findAllByTestId('user-item'));
   expect(userList).toHaveLength(2);
 });
});

И последнее замечание. Поскольку мы имитируем axios, каждый тестовый пример, использующий библиотеку, будет возвращать undefined, если не будет передано фиктивное значение. Итак, чтобы резюмировать наш полный компонентный тест:

import axios from 'axios';
import { render, screen, waitFor } from '@testing-library/react';
import App from './App';

jest.mock('axios');

const fakeUsers = [{
  "id": 1,
  "name": "Test User 1",
  "username": "testuser1",
 }, {
  "id": 2,
  "name": "Test User 2",
  "username": "testuser2",
 }];

describe('App component', () => {
 test('it renders', async () => {
   axios.get.mockResolvedValue({ data: fakeUsers });
   render(<App />);


   expect(screen.getByText('Users:')).toBeInTheDocument();
 });

 test('it displays a list of users', async () => {
   axios.get.mockResolvedValue({ data: fakeUsers });


   render(<App />);


   const userList = await waitFor(() => screen.getByTestId('user-list'));
   expect(userList).toBeInTheDocument();
 });

 test('it displays a row for each user', async () => {
   axios.get.mockResolvedValue({ data: fakeUsers });
   render(<App />);


   const userList = await waitFor(() => screen.findAllByTestId('user-item'));
   expect(userList).toHaveLength(2);
 });
});

Давайте запустим все тесты и посмотрим результаты:

Тестирование React приложений

Заключение

Тестирование вашего приложения React является ключом к созданию высококачественных приложений, а благодаря React, Jest и React Testing Library тестировать наши компоненты и приложения стало проще, чем когда-либо.