Как протестировать приложение React с помощью Jest и библиотеки тестирования React

Автор выбрал Vets Who Code для получения пожертвования в рамках программы Write for DOnations.

Введение

Получение надежного покрытия тестами имеет важное значение для повышения уверенности в вашем веб-приложении. Jest – это средство запуска тестов JavaScript, которое предоставляет ресурсы для написания и выполнения тестов. Библиотека тестирования React предлагает набор вспомогательных средств тестирования, которые структурируют ваши тесты на основе взаимодействия пользователя, а не деталей реализации компонентов. Как Jest, так и библиотека тестирования React поставляются вместе с Create React App и придерживаются принципа того, что тестирование приложений должно напоминать то, как программное обеспечение будет использоваться.

В этом учебном пособии вы протестируете асинхронный код и взаимодействия в образцовом проекте, содержащем различные элементы пользовательского интерфейса. Вы будете использовать Jest для написания и выполнения модульных тестов, а также реализуете библиотеку React Testing Library в качестве вспомогательной библиотеки DOM (Document Object Model) для работы с компонентами.

Предварительные требования

Для завершения этого учебного пособия вам понадобятся:

  • Node.js версии 14 или выше, установленный на вашем локальном компьютере. Чтобы установить Node.js в macOS или Ubuntu 18.04, выполните шаги в разделе Как установить Node.js и создать локальную среду разработки в macOS или разделе Установка с использованием PPA в Как установить Node.js на Ubuntu 18.04.

  • Вам понадобится версия npm 5.2 или выше на вашем локальном компьютере, которую вам потребуется использовать для создания приложения React и npx в образцовом проекте. Если вы не установили npm вместе с Node.js, сделайте это сейчас. Для Linux используйте команду sudo apt install npm.

    • Для работы с пакетами npm в этом руководстве установите пакет build-essential. Для Linux используйте команду sudo apt install build-essential.
  • Git установлен на вашем локальном компьютере. Вы можете проверить, установлен ли Git на вашем компьютере, или пройти процесс установки для вашей операционной системы с помощью Как установить Git на Ubuntu 20.04.

  • Знакомство с React, который вы можете разрабатывать с помощью серии уроков Как программировать на React.js. Поскольку пример проекта создан с использованием Create React App, вам не нужно устанавливать его отдельно.

  • Некоторое знакомство с Jest в качестве тестового раннера или фреймворка полезно, но не обязательно. Поскольку Jest предварительно упакован с Create React App, вам не нужно устанавливать его отдельно.

Шаг 1 — Настройка проекта

На этом этапе вы склонируете образец проекта и запустите набор тестов. Образец проекта использует три основных инструмента: Create React App, Jest и React Testing Library. Create React App используется для инициализации одностраничного приложения React. Jest используется в качестве средства запуска тестов, а React Testing Library предоставляет вспомогательные средства для организации тестов вокруг взаимодействия пользователя.

Для начала вы склонируете предварительно построенное React-приложение с GitHub. Вы будете работать с приложением Doggy Directory, которое является образцовым проектом, использующим Dog API для создания системы поиска и отображения коллекции изображений собак по определенной породе.

Чтобы клонировать проект с GitHub, откройте терминал и выполните следующую команду:

  1. git clone https://github.com/do-community/doggy-directory

Вы увидите вывод, аналогичный этому:

Output
Cloning into 'doggy-directory'... remote: Enumerating objects: 64, done. remote: Counting objects: 100% (64/64), done. remote: Compressing objects: 100% (48/48), done. remote: Total 64 (delta 21), reused 55 (delta 15), pack-reused 0 Unpacking objects: 100% (64/64), 228.16 KiB | 3.51 MiB/s, done.

Перейдите в папку doggy-directory:

  1. cd doggy-directory

Установите зависимости проекта:

  1. npm install

Команда npm install установит все зависимости проекта, определенные в файле package.json.

После установки зависимостей вы можете либо просмотреть развернутую версию приложения, либо запустить приложение локально с помощью следующей команды:

  1. npm start

Если вы выбрали запуск приложения локально, оно откроется по адресу http://localhost:3000/. В терминале вы увидите следующий вывод:

Output
Compiled successfully! You can now view doggy-directory in the browser. Local: http://localhost:3000 On Your Network: http://network_address:3000

После запуска начальная страница приложения будет выглядеть так:

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

  1. npm test

Команда npm test запускает тесты в интерактивном режиме наблюдения с использованием Jest в качестве тестового средства. В режиме наблюдения тесты автоматически перезапускаются после изменения файла. Тесты будут запускаться каждый раз, когда вы изменяете файл, и сообщать вам, прошел ли этот изменение тесты.

После первого запуска npm test вы увидите следующий вывод в терминале:

Output
No tests found related to files changed since last commit. Press `a` to run all tests, or run Jest with `--watchAll`. Watch Usage › Press a to run all tests. › Press f to run only failed tests. › Press q to quit watch mode. › Press p to filter by a filename regex pattern. › Press t to filter by a test name regex pattern. › Press Enter to trigger a test run.

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

Шаг 2 — Тестирование главной страницы

По умолчанию Jest будет искать файлы с суффиксом .test.js и файлы с суффиксом .js в папках __tests__. При изменении соответствующих тестовых файлов они будут обнаружены автоматически. Поскольку тестовые случаи изменяются, вывод будет автоматически обновляться. Файл тестирования, подготовленный для примерного проекта doggy-directory, настроен с минимальным кодом, прежде чем вы добавите парадигмы тестирования. На этом этапе вы напишете тесты, чтобы проверить, что главная страница приложения загрузится перед выполнением поиска.

Откройте src/App.test.js в вашем редакторе, чтобы увидеть следующий код:

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

test('renders the landing page', () => {
  render(<App />);
});

A minimum of one test block is required in each test file. Each test block accepts two required parameters: the first argument is a string representing the name of the test case; the second argument is a function that holds the expectations of the test.

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

Вы добавите блок теста в метод render, который будет проверять, корректно ли отображается стартовая страница до любых вызовов API или сделанных выборов. Добавьте выделенный код под методом render:

src/App.test.js
...
test('renders the landing page', () => {
  render(<App />);
  
  expect(screen.getByRole("heading")).toHaveTextContent(/Doggy Directory/);
  expect(screen.getByRole("combobox")).toHaveDisplayValue("Select a breed");
  expect(screen.getByRole("button", { name: "Search" })).toBeDisabled();
  expect(screen.getByRole("img")).toBeInTheDocument();
});

Функция expect используется каждый раз, когда вы хотите проверить определенный результат, и принимает единственный аргумент, представляющий значение, которое ваш код создает. Большинство функций expect сопровождаются функцией сопоставления, чтобы утверждать что-то о конкретном значении. Для большинства этих утверждений вы будете использовать дополнительные сопоставители, предоставленные jest-dom, чтобы было легче проверить общие аспекты, найденные в DOM. Например, .toHaveTextContent – это сопоставитель для функции expect в первой строке, а getByRole("heading") – это селектор для выбора элемента DOM.

Библиотека тестирования React предоставляет объект screen как удобный способ доступа к соответствующим запросам, необходимым для проверки тестовой среды DOM. По умолчанию библиотека тестирования React предоставляет запросы, которые позволяют находить элементы внутри DOM. Существуют три основные категории запросов:

  • getBy* (наиболее часто используется)
  • queryBy* (используется при тестировании отсутствия элемента без генерации ошибки)
  • findBy* (используется при тестировании асинхронного кода)

Каждый тип запроса служит определенной цели, которая будет определена позже в учебнике. В этом шаге вы сосредоточитесь на запросе getBy*, который является наиболее распространенным типом запроса. Чтобы увидеть исчерпывающий список различных вариаций запросов, вы можете ознакомиться с шпаргалкой по запросам React.

Ниже приведено аннотированное изображение главной страницы Doggy Directory, показывающее каждый раздел, который охватывает первый тест (на отображение главной страницы):

Каждая функция expect утверждает следующее (показано на аннотированном изображении выше):

  1. Вы ожидаете, что элемент с ролью заголовка будет иметь подстроку Doggy Directory.
  2. Вы ожидаете, что выбранный ввод будет иметь точное значение отображения Выберите породу.
  3. Вы ожидаете, что кнопка Поиск будет отключена, так как выбор не был сделан.
  4. Вы ожидаете, что заполнительное изображение будет присутствовать в документе, так как поиск не был выполнен.

Когда закончите, сохраните файл src/App.test.js. Поскольку тесты запускаются в режиме отслеживания изменений, изменения будут регистрироваться автоматически. Если изменения не регистрируются автоматически, возможно, вам нужно будет остановить и перезапустить набор тестов.

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

Output
PASS src/App.test.js ✓ renders the landing page (172 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 2.595 s, estimated 5 s Ran all test suites related to changed files. Watch Usage: Press w to show more.

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

Шаг 3 — Создание заглушки для метода fetch

На этом этапе вы рассмотрите один из подходов к созданию заглушки для метода fetch в JavaScript. Хотя существует множество способов достижения этой цели, в этой реализации будет использоваться метод spyOn и mockImplementation из Jest.

Когда вы зависите от внешних API, есть вероятность того, что их API перестанет работать или займет какое-то время, чтобы вернуть ответ. Замена метода fetch обеспечивает последовательную и предсказуемую среду, что дает вам больше уверенности в ваших тестах. Механизм создания заглушек для API необходим для правильного выполнения тестов, использующих внешний API.

Примечание: В целях упрощения этого проекта вы будете имитировать метод fetch. Однако вам рекомендуется использовать более надежное решение, такое как Mock Service Worker (MSW), при имитации асинхронного кода для более крупных, готовых к производству кодовых баз.

Откройте файл src/mocks/mockFetch.js в вашем редакторе, чтобы ознакомиться с тем, как работает метод mockFetch:

src/mocks/mockFetch.js
const breedsListResponse = {
    message: {
        boxer: [],
        cattledog: [],
        dalmatian: [],
        husky: [],
    },
};

const dogImagesResponse = {
    message: [
        "https://images.dog.ceo/breeds/cattledog-australian/IMG_1042.jpg ",
        "https://images.dog.ceo/breeds/cattledog-australian/IMG_5177.jpg",
    ],
};

export default async function mockFetch(url) {
    switch (url) {
        case "https://dog.ceo/api/breeds/list/all": {
            return {
                ok: true,
                status: 200,
                json: async () => breedsListResponse,
            };
        }
        case "https://dog.ceo/api/breed/husky/images" :
        case "https://dog.ceo/api/breed/cattledog/images": {
            return {
                ok: true,
                status: 200,
                json: async () => dogImagesResponse,
            };
        }
        default: {
            throw new Error(`Unhandled request: ${url}`);
        }
    }
}

Метод mockFetch возвращает объект, который тесно соответствует структуре того, что вызов fetch вернул бы в ответ на вызовы API в приложении. Метод mockFetch необходим для тестирования асинхронной функциональности в двух областях приложения Doggy Directory: выпадающего списка, который заполняет список пород, и вызова API для получения изображений собак при выполнении поиска.

Закройте файл src/mocks/mockFetch.js. Теперь, когда вы понимаете, как будет использоваться метод mockFetch в ваших тестах, вы можете импортировать его в ваш тестовый файл. Функция mockFetch будет передаваться в качестве аргумента методу mockImplementation и затем будет использоваться как фальшивая реализация API fetch.

В файле src/App.test.js добавьте выделенные строки кода для импорта метода mockFetch:

src/App.test.js
import { render, screen } from '@testing-library/react';
import mockFetch from "./mocks/mockFetch";
import App from './App';

beforeEach(() => {
   jest.spyOn(window, "fetch").mockImplementation(mockFetch);
})

afterEach(() => {
   jest.restoreAllMocks()
});
...

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

jest.spyOn(window, "fetch"); создает фальшивую функцию, которая будет отслеживать вызовы метода fetch, присоединенного к глобальной переменной window в DOM.

.mockImplementation(mockFetch); принимает функцию, которая будет использоваться для реализации метода заглушки. Поскольку эта команда переопределяет исходную реализацию fetch, она будет запускаться каждый раз, когда fetch вызывается в коде приложения.

После завершения сохраните файл src/App.test.js.

Теперь, когда вы посмотрите на свои тесты в терминале, вы получите следующий вывод:

Output
console.error Warning: An update to App inside a test was not wrapped in act(...). When testing, code that causes React state updates should be wrapped into act(...): act(() => { /* fire events that update state */ }); /* assert on the output */ This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act at App (/home/sammy/doggy-directory/src/App.js:5:31) 18 | }) 19 | .then((json) => { > 20 | setBreeds(Object.keys(json.message)); | ^ 21 | }); 22 | }, []); 23 | ... PASS src/App.test.js ✓ renders the landing page (429 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.178 s, estimated 2 s Ran all test suites related to changed files.

Предупреждение сообщает вам, что произошло обновление состояния, когда это не ожидалось. Однако вывод также указывает на то, что тесты успешно симулировали метод fetch.

На этом этапе вы замокали метод fetch и включили этот метод в набор тестов. Хотя тест проходит, вам все равно нужно устранить предупреждение.

Шаг 4 — Исправление предупреждения act

На этом этапе вы узнаете, как исправить предупреждение act, возникшее после изменений на шаге 3.

Предупреждение act возникает потому, что вы замокали метод fetch, и при монтировании компонента он делает вызов API для получения списка пород. Список пород хранится в переменной состояния, которая заполняет элемент option внутри ввода select.

Ниже показано, как выглядит ввод select после успешного вызова API для заполнения списка пород:

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

Чтобы исправить эту проблему, добавьте выделенные модификации в тестовый случай в src/App.test.js:

src/App.test.js
...
test('renders the landing page', async () => {
   render(<App />);
   
   expect(screen.getByRole("heading")).toHaveTextContent(/Doggy Directory/);
   expect(screen.getByRole("combobox")).toHaveDisplayValue("Select a breed");
   expect(await screen.findByRole("option", { name: "husky"})).toBeInTheDocument();
   expect(screen.getByRole("button", { name: "Search" })).toBeDisabled();
   expect(screen.getByRole("img")).toBeInTheDocument();
});

Ключевое слово async говорит Jest, что асинхронный код выполняется в результате вызова API при монтировании компонента.

A new assertion with the findBy query verifies that the document contains an option with the value of husky. findBy queries are used when you need to test asynchronous code that is dependent on something being in the DOM after a period of time. Because the findBy query returns a promise that gets resolved when the requested element is found in the DOM, the await keyword is used within the expect method.

По завершении сохраните внесенные изменения в src/App.test.js.

С новыми добавлениями вы увидите, что предупреждение act больше не появляется в ваших тестах:

Output
PASS src/App.test.js ✓ renders the landing page (123 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 0.942 s, estimated 2 s Ran all test suites related to changed files. Watch Usage: Press w to show more.

На этом этапе вы узнали, как исправить предупреждение act, которое может возникнуть при работе с асинхронным кодом. Далее вы добавите второй тестовый случай для проверки интерактивных функций приложения Doggy Directory.

Шаг 5 — Тестирование функции поиска

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

Вернитесь к файлу src/App.test.js в вашем редакторе. В начале файла импортируйте библиотеку user-event и асинхронный метод waitForElementToBeRemoved в файл теста с помощью следующих команд:

src/App.test.js
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react';import userEvent from '@testing-library/user-event'; 
...

Вы будете использовать эти импорты позже в этом разделе.

После первого метода test() добавьте новый асинхронный блок теста и отобразите компонент App с помощью следующего блока кода:

src/App.test.js
...
test("should be able to search and display dog image results", async () => {
   render(<App />);
})

Компонент отображен, теперь вы можете добавить функции, которые проверяют интерактивные функции приложения Doggy Directory.

В файле src/App.test.js добавьте выделенные блоки кода внутри второго метода test():

src/App.test.js
...
test("should be able to search and display dog image results", async () => {
   render(<App />);
   
   //Симулируем выбор опции и проверяем ее значение
   const select = screen.getByRole("combobox");
   expect(await screen.findByRole("option", { name: "cattledog"})).toBeInTheDocument();
   userEvent.selectOptions(select, "cattledog");
   expect(select).toHaveValue("cattledog");
})

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

Запрос getByRole выбирает выбранный элемент и присваивает его переменной select.

Аналогично тому, как вы исправили предупреждение act в Шаге 4, используйте запрос findByRole для ожидания появления опции cattledog в документе перед продолжением дальнейших проверок.

Объект userEvent, импортированный ранее, будет имитировать обычные пользовательские взаимодействия. В этом примере метод selectOptions выбирает опцию cattledog, на которую вы ждали на предыдущей строке.

Последняя строка проверяет, что переменная select содержит выбранное выше значение cattledog.

Следующий раздел, который вы добавите в блок Javascript test(), инициирует запрос на поиск изображений собак на основе выбранной породы и подтверждает наличие состояния загрузки.

Добавьте выделенные строки:

src/App.test.js
...
test("should be able to search and display dog image results", async () => {
   render(<App />);
    
   //...Имитация выбора опции и проверка ее значения

  //Имитация инициирования запроса на поиск
   const searchBtn = screen.getByRole("button", { name: "Search" });
   expect(searchBtn).not.toBeDisabled();
   userEvent.click(searchBtn);

   //Состояние загрузки отображается и удаляется после отображения результатов
   await waitForElementToBeRemoved(() => screen.queryByText(/Loading/i));
})

Запрос getByRole находит кнопку поиска и присваивает ее переменной searchBtn.

Матчер toBeDisabled из библиотеки jest-dom проверит, что кнопка поиска не отключена, когда выбрана порода.

Метод click объекта userEvent имитирует нажатие кнопки поиска.

Функция waitForElementToBeRemoved, импортированная ранее как асинхронный вспомогательный помощник, будет ожидать появления и исчезновения сообщения Loading, пока выполняется вызов поискового API. queryByText внутри обратного вызова waitForElementToBeRemoved проверяет отсутствие элемента без генерации ошибки.

На изображении ниже показано состояние загрузки, которое будет отображаться при выполнении поиска:

Затем добавьте следующий код на JavaScript для проверки изображения и отображения количества результатов:

src/App.test.js
...
test("should be able to search and display dog image results", async () => {
   render(<App />)
   
   //...Симуляция выбора опции и проверка ее значения
   //...Симуляция инициирования запроса на поиск
   //...Отображение состояния загрузки и его удаление после отображения результатов
          
   //Проверка отображения изображения и количества результатов
   const dogImages = screen.getAllByRole("img");
   expect(dogImages).toHaveLength(2);
   expect(screen.getByText(/2 Results/i)).toBeInTheDocument();
   expect(dogImages[0]).toHaveAccessibleName("cattledog 1 of 2");
   expect(dogImages[1]).toHaveAccessibleName("cattledog 2 of 2");
})

Запрос getAllByRole выберет все изображения собак и назначит их переменной dogImages. Вариант запроса *AllBy* возвращает массив, содержащий несколько элементов, соответствующих указанной роли. Вариант *AllBy* отличается от варианта ByRole, который может вернуть только один элемент.

Моковая реализация fetch содержала два URL изображений в ответе. С помощью сопоставителя toHaveLength Jest вы можете проверить, что отображается два изображения.

Запрос getByText будет проверять, что правильное количество результатов появляется в правом верхнем углу.

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

A completed search displaying images of the dog based on the breed selected along with the number of results found will look like this:

Когда вы объедините все части нового кода на JavaScript, файл App.test.js будет выглядеть следующим образом:

src/App.test.js
import {render, screen, waitForElementToBeRemoved} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import mockFetch from "./mocks/mockFetch";
import App from './App';

beforeEach(() => {
   jest.spyOn(window, "fetch").mockImplementation(mockFetch);
})

afterEach(() => {
   jest.restoreAllMocks();
});

test('renders the landing page', async () => {
   render(<App />);

   expect(screen.getByRole("heading")).toHaveTextContent(/Doggy Directory/);
   expect(screen.getByRole("combobox")).toHaveDisplayValue("Select a breed");
   expect(await screen.findByRole("option", { name: "husky"})).toBeInTheDocument()
   expect(screen.getByRole("button", { name: "Search" })).toBeDisabled();
   expect(screen.getByRole("img")).toBeInTheDocument();
});

test("should be able to search and display dog image results", async () => {
   render(<App />);

   //Симулируем выбор опции и проверяем её значение
   const select = screen.getByRole("combobox");
   expect(await screen.findByRole("option", { name: "cattledog"})).toBeInTheDocument();
   userEvent.selectOptions(select, "cattledog");
   expect(select).toHaveValue("cattledog");

   //Инициируем запрос на поиск
   const searchBtn = screen.getByRole("button", { name: "Search" });
   expect(searchBtn).not.toBeDisabled();
   userEvent.click(searchBtn);

   //Состояние загрузки отображается и удаляется после отображения результатов
   await waitForElementToBeRemoved(() => screen.queryByText(/Loading/i));

   //Проверяем отображение изображения и количество результатов
   const dogImages = screen.getAllByRole("img");
   expect(dogImages).toHaveLength(2);
   expect(screen.getByText(/2 Results/i)).toBeInTheDocument();
   expect(dogImages[0]).toHaveAccessibleName("cattledog 1 of 2");
   expect(dogImages[1]).toHaveAccessibleName("cattledog 2 of 2");
})

Сохраните внесённые изменения в файле src/App.test.js.

Когда вы проверите свои тесты, окончательный вывод в терминале будет иметь следующий вид:

Output
PASS src/App.test.js ✓ renders the landing page (273 ms) ✓ should be able to search and display dog image results (123 ms) Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 4.916 s Ran all test suites related to changed files. Watch Usage: Press w to show more.

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

Заключение

На протяжении этого учебного пособия вы писали тестовые случаи, используя Jest, React Testing Library и совпадения jest-dom. Постепенно строя тесты, вы писали их на основе того, как пользователь взаимодействует с пользовательским интерфейсом. Вы также узнали различия между запросами getBy*, findBy* и queryBy*, а также умения тестировать асинхронный код.

Чтобы узнать больше о упомянутых выше темах, посмотрите официальную документацию по Jest, React Testing Library и jest-dom. Вы также можете прочитать статью Кента К. Додда Common Mistakes with React Testing Library, чтобы узнать о лучших практиках при работе с React Testing Library. Чтобы узнать больше о использовании снимков тестов в приложении React, ознакомьтесь с руководством How To Write Snapshot Tests.

Source:
https://www.digitalocean.com/community/tutorials/how-to-test-a-react-app-with-jest-and-react-testing-library