Повний посібник з тестування застосунків на React

26 хв. читання

Спробуйте запитати у розробників про основні переваги автоматизованого тестування. Імовірно, ви почуєте десь такі аргументи:

  • тестування дає впевненість в тому, що навіть один змінений рядок коду не зламає весь застосунок;
  • замінює ручне тестування, яке з часом стає обтяжливим;
  • допомагає задокументувати граничні випадки;
  • розробники-новачки, які мають досвід написання тестів, часто отримують перевагу над іншими кандидатами.

Для розробників-новачків тестування може здатись нелегкою справою. Це щось схоже на розробку в зовсім новому середовищі. Ви вже звикли до інструментів розробника в браузері, а тут вони зовсім інші. До того ж ви, може, й не знаєте, що саме тестувати.

Насправді це не так вже й складно. З правильним підходом ви легко почнете писати тести, навіть без попереднього досвіду. Сподіваємось, в цьому вам також допоможе цей матеріал. Для прикладу розглянемо невеликий застосунок і потестуємо його з усіх боків. Обговоримо, що та чому необхідно в ньому тестувати, оглянемо різні способи, які допоможуть писати тести впевненіше.

Для тих, хто не любить довгих матеріалів, автор статті підготував шпаргалку з порадами про тестування та список корисних ресурсів.

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

Принципи тестування React-застосунків

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

Мета тестування — переконатися, що застосунок працює коректно. Якщо всі важливі частини функціоналу застосунку мають тести, ви одразу дізнаєтесь, коли щось зламалось.

Поширеними інструментами автоматизованого тестування застосунків на React є Jest у поєднанні з @testing-library/react (або Testing Library). Існують і їхні аналоги. Наприклад, аналогами Jest є Mocha, Jasmine та AVA. Testing Library — альтернатива для Enzyme, яку багато розробників досі використовують.

Testing Library розглядає тестування застосунку з погляду користувача. Наслідком такого підходу стає написання інтеграційних тестів, де кілька компонентів системи взаємодіють між собою.

Для прикладу уявімо кнопку. З Testing Library ви не будете перевіряти, чи викликалась функція onClick при кліці на кнопку. Імовірніше, що ви тестуватимете, чи призводить клік на певну кнопку до відповідного стороннього ефекту. Скажімо, кнопка видалення відкриває відповідне модальне вікно.

Застосунок для тестування

Повний посібник з тестування застосунків на React

Основним призначенням тестового застосунку є пошук найпопулярніших публікацій з Reddit. Сам застосунок досить простий, в ньому є лише заголовок з декількома посиланнями та форма. Однак такого функціоналу достатньо, аби почати писати тести.

Посилання в заголовку направляють на інші сторінки, вміст яких не важливий для наших тестів. У формі пошуку є одне поле вводу, де користувач може ввести категорію для пошуку. Кліком на кнопку submit ми відправляємо запит на Redit API. Поки застосунок очікує відповіді, показується стан очікування. Щойно дані отримані, показуємо їх (в нашому випадку — це кількість публікацій).

Отримати повний код застосунку ви можете знайти в репозиторії за посиланням.

Що треба протестувати

Це перше запитання, яке виникає у початківця. Для прикладу розглянемо форму. Таким буде сам компонент:

function Form({ onSearch }) {
  const [subreddit, setSubreddit] = useState('javascript');

  const onSubmit = (event) => {
    event.preventDefault();
    onSearch(subreddit);
  };

  return (
    <FormContainer onSubmit={onSubmit}>
      <Label>
        r /
        <Input
          type="text"
          name="subreddit"
          value={subreddit}
          onChange={(event) => setSubreddit(event.target.value)}
        />
      </Label>

      <Button type="submit">
        Search
      </Button>
    </FormContainer>
  );
}

Форма зберігає стан у змінній та керує ним. Після кліку на кнопку submit викликається метод onSearch, який передається від батьківського компонента.

Вам може бути цікаво, як саме було отримано дані. Логіка запиту розміщена в компоненті домашньої сторінки, який є батьківським для форми.

function Home() {
  const [posts, setPosts] = useState([]);
  const [status, setStatus] = useState('idle')

  const onSearch = async (subreddit) => {
    setStatus('loading');
    const url = `https://www.reddit.com/r/${subreddit}/top.json`;
    const response = await fetch(url);
    const { data } = await response.json();
    setPosts(data.children);
    setStatus('resolved');
  };

  return (
    <Container>
      <Section>
        <Headline>
          Find the best time for a subreddit
        </Headline>

        <Form onSearch={onSearch} />
      </Section>

      {
        status === 'loading' && (
          <Status>
            Is loading
          </Status>
        )
      }
      {
        status === 'resolved' && (
          <TopPosts>
            Number of top posts: {posts.length}
          </TopPosts>
        )
      }
    </Container>
  );
}

Компонент головної сторінки зберігає відповідь API у змінній стану, а також займається обробкою стану очікування. Коли форма ініціює пошук, відправляється запит на Reddit API. Одразу як дані приходять, обидві змінні стану оновлюються, а результат показується під формою.

Тепер ми розглянули основні частини застосунку. Перш ніж перейдемо далі, спробуйте відповісти для себе: як би ви протестували ці два компоненти?

Зважаючи на функціонал компонентів, спадає на думку перевірити, чи правильно встановлюється стан, якщо проп onSearch компонента Form викликається з поточним значенням. Більшість розробників звикли писати такі тести на Enzyme.

Але з Testing Library у нас немає доступу до стану. Ми досі можемо тестувати пропси, але не можемо дізнатися, яке конкретно значення зберігає змінна стану.

Можна подумати, що це недолік, однак насправді це перевага. Логіка управління станом розкриває внутрішню реалізацію компонента. Ми можемо перемістити стан форми до її батьківського компонента — і застосунок працюватиме так само.

Взагалі, React сам по собі — це деталь реалізації. Ми можемо переписати весь застосунок на Vue.js і далі мати той самий функціонал.

Замість фокусуватись на коді і тому, як він працює, ми просто приймаємо позицію користувача. Так ми вимушені зосередитись на важливих частинах застосунку.

Щойно ви осягнете таку філософію тестування, воно вже не здаватиметься вам таємничим та лячним.

Зверніть увагу: користувач у нашому випадку — це не лише кінцевий користувач застосунку, а й інший розробник, який послуговується вашим компонентом. Припустимо, ви створюєте галерею зображень, яка потрібна й іншим розробникам в команді. Вам потрібно протестувати, чи вона поводиться коректно, якщо, наприклад, змінились певні властивості.

Тож забудемо на деякий час про реалізацію компонента та уявімо себе його користувачем. Який тоді функціонал стане для нас важливим?

Повний посібник з тестування застосунків на React

Розглянемо простий сценарій взаємодії із застосунком:

  1. Користувач вводить значення в поле форми та відправляє його.
  2. Застосунок показує повідомлення очікування, доки завантажуються дані.
  3. Коли мережева відповідь приходить, дані показуються.

Користувачу не важливо, що саме відповідає за збереження значення вводу — головна сторінка чи компонент форми. Немає значення, зберігаються публікації в змінній стану чи ні, яка структура даних для цього збереження використовується. Користувача турбують лише пункти, які ми описали вище.

Звичайно, нам треба також протестувати посилання в заголовку. Некоректне посилання може мати досить негативні наслідки для будь-якого застосунку.

Зверніть увагу: зазвичай у формах варто тестувати граничні випадки та обробку помилок. Однак ця тема залишиться для майбутніх матеріалів.

Пишемо тести

Пригадаймо трохи наші висновки з попередньої частини й перекладемо їх технічною мовою.

Ми напишемо два тестових випадки. Один — для посилань в заголовку, а інший — для форми. В першому випадку необхідно протестувати, чи вказують посилання на коректні сторінки. У випадку з формою протестуємо процес її зміни та надсилання значення, появи стану завантаження та виведення результату.

Почнемо з першого тест-кейсу. Відкриймо файл src/App.test.js та видалимо наявний вміст. Тепер опишемо наш тестовий випадок у блоці describe() Jest.

Зверніть увагу: огортати тести у describe не обов'язково, однак це дає змогу згрупувати їх за призначенням. На думку автора, це робить результат більш організованим та читабельним, запобігає повторенню тестових повідомлень, дає змогу приховати згруповний блок тестів в редакторі коду.

describe('Header', () => {

});

Тестові кейси оголошуються конструкцією test(). Як альтернативу можна використовувати it(). Обидві функції пропонуються бібліотекою Jest.

describe('Header', () => {
  test('"How it works" link points to the correct page', () => {

  });
});

Нам не треба тестувати Header в ізоляції. Нам потрібно розглянути його в контексті застосунку. Тому для тесту використовуємо компонент App. Він реалізований так:

import React from 'react';
import { Switch, Route } from 'react-router-dom';
import GlobalStyle from './GlobalStyle';
import Header from './components/Header';
import Home from './pages/Home';

function App() {
  return (
    <>
      <GlobalStyle />
      <Header />

      <main>
        <Switch>
          <Route path="/how-it-works">
            <h1>How it works</h1>
          </Route>
          <Route path="/about">
            <h1>About</h1>
          </Route>
          <Route path="/">
            <Home />
          </Route>
        </Switch>
      </main>
    </>
  );
}

Як і більшість продакшен-застосунків, в компоненті App ми взаємодіємо з React Router. Компонент рендерить заголовок та декілька роутів, один з яких — головна сторінка.

Зверніть увагу, що в коді немає компонента Router. Для цілей тестування виносимо його в index.js, а компонент App огортаємо в MemoryRouter.

В першу чергу рендеримо компонент App. У Testing Library є функція render, яка формує DOM для переданого компонента.

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

describe('Header', () => {
  test('"How it works" link points to the correct page', () => {
    render(
      <MemoryRouter>
        <App />
      </MemoryRouter>
    );
  });
});

Оскільки застосунок створювався з create-react-app, все потрібне для використання Testing Library вже встановлено автоматично.

Не починайте бій у темряві

Коли ви лише починаєте писати тести, то іноді здається, наче ви працюєте з чорним ящиком: не дуже зрозуміло, що ж відбувається. Ви ж бо звикли працювати з браузером, де можна відкрити інструменти розробника й дослідити дерево DOM.

Коли ви пишете тести, то маєте звикати до нового середовища. Потрібен час, аби зрозуміти, що до чого. Що робити, якщо раптом тест провалюється, тому що елемент не знайдено?

Тоді на допомогу приходить функція debug, яка в будь-який момент може вивести поточне DOM-дерево застосунку. Звісно, це не так зручно, як робота з інструментами браузера, але це допоможе з'ясувати, що відбувається.

Якщо ви тільки почали писати тести, не варто покладатись на метод спроб і помилок. Краще витратьте трохи більше часу, щоб використати debug після кожного кроку, це дасть кращу картину.

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

describe('Header', () => {
  test('"How it works" link points to the correct page', () => {
    render(
      <MemoryRouter>
        <App />
      </MemoryRouter>
    );
    screen.debug();
  });
});

Якщо запустити тест з yarn test, побачимо такий результат:

Повний посібник з тестування застосунків на React

Тут є заголовок, який містить декілька посилань, серед яких посилання «How it works», яке ми хочемо протестувати.

Як отримати доступ до дерева DOM у застосунку

Для доступу до відрендерених елементів рекомендується використовувати об'єкт screen, який експортується з Testing Library.

Цей об'єкт містить декілька функцій для доступу до елементів DOM. Серед них:

  • getBy: функції на зразок getByTestId, getByText або getByRole. Вони синхронні, а їхня основна мета — перевірити, чи міститься елемент в DOM. Якщо ні, повертається помилка.
  • findBy: функції на зразок findByText. Вони асинхронні, тому чекають деякий час (автоматично 5 секунд), доки елемент з'явиться в DOM. Якщо ні, також буде викликано помилку.
  • queryBy функції, котрі належать до синхронних, як і getBy, але вони не викликають помилку, якщо елемента немає, а просто повертають null.

Ми вже перерахували багато функцій для взаємодії з DOM, та на цьому перелік не завершується.

Як бачимо, заголовок завжди розташований на сторінці. Нам не треба очікувати, коли він з'явиться. Тому для доступу до заголовка чудово підійдуть функції getBy. Яку ж саме обрати?

На перший погляд здається, що гарним вибором буде getByTestId. Нам просто треба додати до елемента тестовий ідентифікатор:

<div data-testid="some-content">
  Some content
</div>

Тепер ми можемо отримати доступ до елемента div так: getByTestId('some-content'). Виглядає досить просто, чи не так? Але з таким підходом вам доведеться змінювати наявний код. Це, очевидно, не ідеальний спосіб. Щоб знайти накращий варіант, варто звернутись до документації Testing Library, вона описує, коли варто використовувати ту чи ту функцію для пошуку елемента. Найчастіше це getByRole. Не зловживайте функціями на зразок getByAltText або getByTitle. А от getBytestId варто використовувати, лише якщо жодного іншого варіанту немає.

Спробуймо використати функцію getByRole для пошуку нашого компонента. Першим параметром передається ARIA-роль елемента. В нас це посилання. Оскільки в нас декілька посилань на сторінці, нам треба детальніше схарактеризувати елемент властивістю name.

render(
  <MemoryRouter>
    <App />
  </MemoryRouter>
);

const link = screen.getByRole('link', { name: /how it works/i });

Тут спеціально використовується регулярний вираз /how it works/i, а не звичайний рядок 'How it works', щоб уникнути проблем з регістром (наприклад, якщо ви використовуєте CSS для перетворення тексту). До того ж з регулярним виразом можна шукати частини рядків.

При збереженні файлу тести автоматично запускаються повторно. Якщо ваш тест не пройшов, згадайте про налагодження та функцію debug. Не всі знають, що вона приймає як параметр елемент та виводить його у консоль.

const link = screen.getByRole('link', { name: /how it works/i });
screen.debug(link);

Ваш результат буде приблизно таким:

Повний посібник з тестування застосунків на React

Взаємодія з DOM-елементами

У попередній частині ми розглянули, як отримати доступ до елементів DOM. Однак цього не достатньо для нашого тест-кейсу.

Щоб посилання відкрило нову сторінку, необхідно на нього клікнути. З Testing Library у нас є два варіанти:

  1. Використати функцію fireEvent.click з модуля @testing-library/react.
  2. Використати функцію click з модуля @testing-library/user-event.

Рекомендується використовувати модуль @testing-library/user-event, де можливо. Там більше функцій, що імітують дії користувача. Якщо ви розглянете package.json, то побачите, що цей модуль встановлено автоматично при конфігурації застосунку з create-react-app.

Напишемо код, який імітуватиме клік на наше посилання:

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

describe('Header', () => {
  test('"How it works" link points to the correct page', () => {
    render(
      <MemoryRouter>
        <App />
      </MemoryRouter>
    );
    const link = screen.getByRole('link', { name: /how it works/i });
    userEvent.click(link);
  });
});

В пошуках потрібної сторінки

Тепер нам треба перевірити, чи відкривається потрібна сторінка після переходу за посиланням.

Для цього можна перевірити URL, як описано в документації react-router. Однак для користувача URL не надто важливий. До того ж він може бути цілком коректний, а от замість потрібної сторінки буде «404 не знайдено».

В браузері коректна поведінка переходу за посиланням повинна бути такою:

Повний посібник з тестування застосунків на React

Після кліку на посилання ми очікуємо побачити сторінку із заголовком «How it works». Якщо заголовок має ARIA-роль, ми знову можемо використати getByRole для перевірки. Відповідно до документації MDN, ARIA-роль заголовку — це heading:

userEvent.click(link);

screen.getByRole('heading', { name: /how it works/i });

Наш тест пройшов, тобто потрібний заголовок є в документі й ми на потрібній сторінці.

І наостанок: не варто використовувати getBy, щоб дізнатись, чи показується елемент. Краще використати expect(...).toBeInDocument().

Повний тест:

test('"How it works" link points to the correct page', () => {
  render(
    <MemoryRouter>
      <App />
    </MemoryRouter>
  );

  const link = screen.getByRole('link', { name: /how it works/i });
  userEvent.click(link);

  expect(
    screen.getByRole('heading', { name: /how it works/i })
  ).toBeInTheDocument();
});

Хоча тест лаконічний, ми досить довго розбирали його особливості. Та щойно ви зрозумієте сутність тестування, написання такого тесту стане надзвичайно простим.

Перш ніж перейдемо до тестування форми, нагадаймо, що ми протестували одне посилання. Ви також могли побачити лого з лівого боку заголовка, яке направляє на головну сторінку, а також посилання, яке веде на сторінку «About».

Напишіть аналогічні тести, щоб закріпити отримані навички. Дві поради для вас:

  1. Аби отримати доступ до посилання, яке огортає лого, можна використати функцію getByRole('link', { name }). Якщо не знаєте, що передати як параметр name, використайте screen.debug().
  2. Тести для посилань «How it works» та «About» можна скомбінувати за допомогою test.each.

Тестування форми

Час перейти до більш складного тестування форми. Нагадаймо, як користувач взаємодіє з формою у нашому застосунку:

Повний посібник з тестування застосунків на React

Цей тест-кейс складається з таких кроків:

  1. Користувач вводить значення у полі форми та клікає «submit».
  2. Застосунок показує стан завантаження, доки чекає на дані.
  3. Коли відповідь приходить, показуються отримані дані.

Почнемо писати тест так само, як і для заголовка:

describe('Subreddit form', () => {
  test('loads posts that are rendered on the page', () => {
    render(
      <MemoryRouter>
        <App />
      </MemoryRouter>
    );
  });
});

Не повторюємось із функцією setup

Як бачимо, частина коду, що відповідає за рендер компонентів, дублюється в обох тестах. Аби уникнути повторень, створюємо функцію setup:

function setup() {
  return render(
    <MemoryRouter>
      <App />
    </MemoryRouter>
  );
}

...

describe('Subreddit form', () => {
  test('loads posts and renders them on the page', () => {
    setup();
  });
});

Тепер просто викликаємо функцію setup на початку кожного тесту.

Взаємодія з формою

Повернемось до першого кроку тест-кейсу, а саме:користувач вводить значення у полі форми та клікає «submit».

Перш ніж ми спробуємо отримати доступ до поля вводу, використаємо screen.debug() для виводу елементів застосунку:

Повний посібник з тестування застосунків на React

Бачимо, що поле вводу має позначку r/. Ще раз звернемось до переліку пріоритетів функцій доступу до елементів. Бачимо, що функція getByLabelText — потрібний спосіб для знаходження поля форми.

Щоб взаємодіяти з полем та вводити туди значення, можна використати функцію type з @testing-library/user-event.

setup();

const subredditInput = screen.getByLabelText('r /');
userEvent.type(subredditInput, 'reactjs');

Далі треба відправити дані форми. Якщо повернутись до результату функції screen.debug(), можна побачити, що форма рендерить кнопку. Для такого елемента підходить функція getByRole:

const subredditInput = screen.getByLabelText('r /');
userEvent.type(subredditInput, 'reactjs');

const submitButton = screen.getByRole('button', { name: /search/i });
userEvent.click(submitButton);

screen.debug();

В кінці тесту ми додали ще один виклик debug, аби побачити, яким буде застосунок на цьому етапі.

Повний посібник з тестування застосунків на React

Бачимо, що застосунок рендерить напис «Is loading». А це саме та поведінка, на яку ми очікуємо після надсилання даних форми.

Повний посібник з тестування застосунків на React

Доступ до елемента без ARIA-ролі

Другим кроком нашого тест-кейсу було повідомлення про завантаження, коли триває очікування даних.

Оскільки повідомлення про завантаження огортається у звичайний div, у нас немає ARIA-ролі для доступу до такого елемента. Відповідно до документації Testing Library, в такому випадку краще використовувати функцію getByText.

userEvent.click(submitButton);

expect(screen.getByText(/is loading/i)).toBeInTheDocument();

Тим часом переходимо до наступного кроку: показуємо дані, коли приходить відповідь з сервера.

Очікування даних

Ми вже відправили дані форми й побачили повідомлення про завантаження. А це означає, що API-запит здійснено, однак застосунок досі не отримав результат. Щоб перевірити, чи правильно показуватимуться отримані дані, треба почекати на відповідь.

Досі ми мали справу лише з функціями getBy. Але на початку з'ясували, що ці функції синхронні. Тобто вони взаємодіють з поточним станом застосунку: якщо потрібного елемента немає, виникає помилка.

Саме час перейти до іншого типу запитів: асинхронні функції findBy, які чекають щонайбільше 5 секунд, доки елемент з'явиться на сторінці.

Перш ніж перейдемо до цього, визначимо ідентифікатор елемента. Ми знаємо, що застосунок показує кількість популярних дописів під формою, коли результат виконується успішно. Текст, який ми очікуємо побачити:«Number of top posts: ...». Тож нам підійде функція findByText.

Оскільки ми наперед не знаємо, скільки буде дописів, використаємо регулярний вираз. Пам'ятаєте, що регулярні вирази дозволяють знайти рядок за його частиною?

test('loads posts and renders them on the page', async () => {
  setup();

  const subredditInput = screen.getByLabelText('r /');
  userEvent.type(subredditInput, 'reactjs');

  const submitButton = screen.getByRole('button', { name: /search/i });
  userEvent.click(submitButton);

  const loadingMessage = screen.getByText(/is loading/i);
  expect(loadingMessage).toBeInTheDocument();

  const numberOfTopPosts = await screen.findByText(/number of top posts:/i);
  screen.debug(numberOfTopPosts);
});

Оскільки функція findByText асинхронна, нам потрібно використати await. Для цього додаємо ключове слово async перед тестовою функцією.

Результат debug функції на цьому етапі:

Повний посібник з тестування застосунків на React

Тест пройшов, адже дані результату було показано. Тож ми протестували такі кроки взаємодії з формою:

  1. Користувач вводить значення у полі форми та натискає «submit».
  2. Застосунок показує стан завантаження, доки чекає на дані.
  3. Коли відповідь нарешті приходить, рендеряться відповідні дані.

Однак це не все, залишилась невелика деталь.

Імітація API-запитів

Ви, мабуть, помітили, що виконання тесту займає досить багато часу. Все тому, що ми надсилаємо реальний запит на Reddit API. Таке не рекомендується робити під час інтеграційних тестів з декількох причин:

  1. API-запити займають багато часу. Зазвичай інтеграційні тести запускаються на локальній машині, перш ніж відправити код до репозиторію. Також часто такі тести запускаються під час CI-процесів. Коли у вас багато тестів із реальними запитами, їх виконання триває досить довго і негативно впливає на швидкість доставлення коду.
  2. Ми не можемо контролювати API-запити. В інтеграційних тестах ми хочемо протестувати різні стани застосунку. Наприклад, перевірити, як буде поводитись застосунок, якщо сервери не відповідатимуть. Зазвичай ми не можемо спровокувати збій сервера під час тесту. Однак ми легко можемо імітувати будь-який тип відповіді із замоканим запитом.
  3. Наш тест може не пройти, навіть якщо з кодом все добре. Усе тому, що API не відповів так, як ми очікували (якщо сервер вийшов з ладу, наприклад). Для таких ситуацій також слід написати тести, однак це будуть тести end-to-end, а не інтеграційні.

Сподіваємось, ви зрозуміли, чому важливо мокати API-запити. Перейдемо до нашого тесту.

Аби замокати запит, найперше слід дізнатись, як він надсилається. Реалізація запиту розташована в компоненті Home.

function Home() {
  const [posts, setPosts] = useState([]);
  const [status, setStatus] = useState('idle')

  const onSearch = async (subreddit) => {
    setStatus('loading');
    const url = `https://www.reddit.com/r/${subreddit}/top.json`;
    const response = await fetch(url);
    const { data } = await response.json();
    setPosts(data.children);
    setStatus('resolved');
  };
  
  ...

Щоб замокати запит з fetch, можна використати npm-пакет jest-fetch-mock. Спочатку встановимо його:

yarn jest-fetch-mock

Тепер імпортуємо та ініціалізуємо модуль:

import fetchMock from 'jest-fetch-mock';

fetchMock.enableMocks();

Зараз наш тест не буде проходити, адже ми ще не повідомили замоканому запиту, які дані віддавати.

Щоб створити замокану відповідь сервера, треба перейти до браузера. Відкриваємо вкладку Network в dev-tools, відправляємо дані форми й копіюємо відповідь, яка прийшла.

Повний посібник з тестування застосунків на React

Тепер створюємо json-файл та вставляємо туди відповідь сервера. Далі просто викликаємо fetch.once, аби передати імітовану відповідь:

import mockResponse from './__mocks__/subreddit-reactjs-response.json';

...

test('loads posts and renders them on the page', async () => {
  fetch.once(JSON.stringify(mockResponse));
  setup();
  ...

Нарешті тест проходить. Оскільки ми використовуємо імітовану відповідь, яку можемо самостійно контролювати, то можна більш конкретизувати наш тест і очікувати на точну кількість дописів (в нашому випадку це 25).

expect(await screen.findByText(/number of top posts: 25/i)).toBeInTheDocument();

Зверніть увагу: якщо ваш запит відправляє багато API-запитів, то їх імітація може бути громіздким процесом. Полегшити його допоможе пакет MSW. Більше інформації про нього за посиланням.

Тестуємо замокані функції

Наостанок нам варто перевірити, чи викликаються потрібні API-ендпоінти. Так ми переконаємось, що користувачі побачать потрібні дані.

Оскільки ми використовуємо jest-mock-fetch, то функція fecth мокається глобально. Тож для нашого тесту ідеально підійде функція Jest toHaveBeenCalledWith:

expect(fetch).toHaveBeenCalledWith('https://web.archive.org/web/20230331010702/https://www.reddit.com/r/reactjs/top.json');

Зверніть увагу: деколи вам доведеться мокати функції самостійно. З Jest ви можете просто створити нову замокану функцію за допомогою jest.fn(). Під капотом jest-mock-fetch також використовує такий підхід.

Повний код тесту буде ось таким:

describe('Subreddit form', () => {
  test('loads posts and renders them on the page', async () => {
    fetch.once(JSON.stringify(mockResponse));
    setup();

    const subredditInput = screen.getByLabelText('r /');
    userEvent.type(subredditInput, 'reactjs');

    const submitButton = screen.getByRole('button', { name: /search/i });
    userEvent.click(submitButton);

    expect(screen.getByText(/is loading/i)).toBeInTheDocument();

    expect(await screen.findByText(/Number of top posts: 25/i)).toBeInTheDocument();
    expect(fetch).toHaveBeenCalledWith('https://web.archive.org/web/20230331010702/https://www.reddit.com/r/reactjs/top.json');
  });
});

Післямова

Якщо ви прочитали весь матеріал, то вже зробили великий крок вперед до впевненого тестування застосунків.

Пам'ятайте про такі важливі моменти:

  1. Тестуйте застосунок з погляду користувача.
  2. Використовуйте screen.debug(), якщо не впевнені, що відбувається із застосунком.
  3. Використовуйте функції getByRole, findByRole, щоб отримати доступ до дерева DOM, де це можливо.

Якщо ви хотіли б ще почитати на тему тестування, то більш просунутий матеріал про рефакторинг та налагодження тестів за посиланням.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.6K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація