Введение

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

"Первая часть"

"Вторая часть"

"Третья часть"

Модульный тест – это метод тестирования изолированного и отдельного фрагмента кода, модуля. В контексте React это, скорее всего, относится к тестированию наших компонентов изолированно и любой связанной с ними функции

Замечательно! Но как мы решаем, что именно тестировать? Наши тесты должны касаться только тех взаимодействий, которых ожидает пользователь. Детали реализации, такие как имена переменных, имена функций и т. д., не должны иметь значения в наших тестах.

Решение о модульном тесте

Чтобы дать краткий обзор, мы создали очень простое приложение, которое извлекает некоторый общий JSON и отображает его на странице:

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

  1. При нажатии на кнопку получения сообщений он должен перечислить сообщения.
  2. При нажатии на кнопку очистки сообщений он должен очистить сообщения.

Что нас не волнует:

  1. Имя функции, вызывающей запрос на выборку
  2. Имена классов элементов в содержимом

Глядя на наш код

Давайте сравним это, взглянув на наш код.

<section className="App-buttons">     
     <button onClick={fetchPosts} type="button">       
       Fetch Posts     
     </button>     
     <button onClick={clearPosts} type="button">       
       Clear posts     
     </button> 
</section> 
{loading && <p>Loading...</p>} 
{posts.map((post) => (     
    <article key={post.id}>       
      <h3>{post.title}</h3>       
      <p>{post.body}</p>     
    </article>   ))} 
</main>

Итак, чтобы визуализировать это, как если бы мы были конечным пользователем:

Что здесь не так? Что ж, мы тестируем некоторые детали реализации, такие как содержание ответа и то, была ли функция выполнена.

Эти части должны стать черным ящиком для наших тестов.

Что-то лучше было бы:

Вы можете спросить, а разве первый вариант не проверяет больше?

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

В конце концов, нам нужно, чтобы сообщения извлекались и отображались. Детали функции не имеют значения.

Изменение нашего приложения для первого модульного теста

Давайте изменим наш файл vite.config.js:

import react from '@vitejs/plugin-react';  
// https://vitejs.dev/config/ 
export default defineConfig({  
    plugins: [react()],  
    test: {   
        globals: true,   
        environment: 'jsdom',  
    }, 
});

Написание модульного теста

Давайте продолжим и создадим наш первый тест. В корне нашего проекта создадим App.test.js

import { describe } from 'vitest';  
describe('Testing our React application', () => {  
    it('Fetch posts', () => {}); 
});

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

vitest --watch

Это должно привести к ошибке, поскольку в нашем тесте нет утверждений:

Далее, чтобы отрендерить наши компоненты, нам понадобится помощь еще одной библиотеки: React-testing-library.

Семейство пакетов @testing-library помогает тестировать компоненты пользовательского интерфейса с учетом интересов пользователя.

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

Во-первых, давайте просто проверим, что мы можем правильно отрендерить и пройти тест:

import React from 'react'; 
import { describe, expect, it } from 'vitest'; 
import { render, screen } from '@testing-library/react'; 
import '@testing-library/jest-dom'; 
import App from './App';  
describe('Testing our React application', () => {  
    it('Fetch posts', async () => {   
       render(<App />);    
       expect(screen.getByText(/Modern React Testing/i)).toBeInTheDocument();  
    }); 
});

Здесь мы просто визуализируем наше приложение и проверяем заголовок нашего заголовка. Ожидание в данном случае — это наше утверждение, которое решает, пройдём мы тест или нет.

Модульный тест для получения

Но это не имеет отношения к реальному тестированию. Итак, давайте попробуем поработать с нашей кнопкой и функциями поствыборки.

import React from 'react'; 
import { describe } from 'vitest'; 
import { render, screen } from '@testing-library/react'; 
import userEvent from '@testing-library/user-event'; 
import '@testing-library/jest-dom'; 
import App from './App';  
describe('Testing our React application', () => {  
    it('Fetch posts', () => {   
       const user = userEvent.setup();   
       render(<App />);    
       expect(screen.getByText(/Modern React Testing/i)).toBeInTheDocument();  
     }); 
});

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

Например, внутри нашего объекта userEvent у нас есть доступ к функции click! И с помощью этой функции щелчка мы можем отправить аргумент для поиска нашей кнопки.

userEvent.click(screen.getByRole('button', { name: 'Fetch Posts'}));

Поясним это на схеме:

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

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

import { describe } from 'vitest'; 
import { render, screen } from '@testing-library/react'; 
import { userEvent } from '@testing-library/user-event'; 
import App from './App';  
describe('Testing our React application', async () => {    
     it('Fetch posts', () => {       
        render(<App />);  
        userEvent.click(screen.getByRole('button', { name:'Fetch Posts'}));
        await waitForElementToBeRemoved(() =>   screen.queryByLabelText('Loading...') );
        expect(screen.getByRole('heading', { level: 3 })).toBeDefined();     
     }); 
});

Идеальный. И снова мы используем функциональность экрана и просто ждем, пока исчезнет текст «Загрузка».

НО, это выдаст нам ошибку… но почему? Что ж, мы не заглушаем и не издеваемся над сервисом выборки, который вызывается при нажатии кнопки. Как мы можем это решить?

Завершение

В следующей статье я расскажу, как использовать MSW для имитации запросов, запускаемых из тестов. Мы рассмотрим настройку и интегрируем ее с этим тестом!

Больше контента на Relatable Code

Давайте подключим

Если вам это понравилось, не стесняйтесь связаться со мной в LinkedIn или Twitter.

Ознакомьтесь с моей дорожной картой бесплатного разработчика и еженедельными новостями технологической отрасли в моем рассылках.