Написание интеграционного теста с библиотекой тестирования React

Этот пост изначально был опубликован на моем личном сайте.

Думаю, что сейчас мне не нужно объяснять важность тестирования ваших приложений. Автоматические тесты позволяют вам выявлять ошибки во время разработки и избегать звонков в 2 часа ночи, потому что ваше приложение не работает должным образом.

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

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

Но можно ли провести слишком много тестов?

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

Некоторое время назад Гильермо Раух написал в Твиттере:

Напишите тесты. Не так много. В основном интеграция.

Мы часто слышим, что наши тесты должны покрывать 100% нашего кода. И это не всегда хорошая идея. Всегда есть точка, которая является поворотной точкой. Как только вы получите этот X% покрытия, новые тесты, которые вы пишете, действительно не помогают. Число разное для каждого проекта, но оно никогда не достигает 100%.

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

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

Хорошо, а почему интеграционные тесты?

Три наиболее распространенных типа тестов: модульные, интеграционные и сквозные.

Модульные тесты быстрее и, конечно же, дешевле. Но они также не вселяют в вас уверенности. Это отличное тестирование, если компонент A отображается правильно, но если вы не также протестируете его вместе с B и C, вы не будете полностью уверены в своем приложении.

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

Интеграционные тесты - это идеальный баланс. Вы тестируете как ваши компоненты работают вместе, но имитируете некоторые вещи (например, HTTP-запросы), поэтому не займет у вас много времени, чтобы их написать. К тому же они придают вам большую уверенность. Как только вы начнете тестировать свои компоненты таким образом, вы увидите, что вам может вообще не понадобиться много модульных тестов.

Вот почему неплохо тратить большую часть времени на написание интеграционных тестов.

Это не означает, что вы должны только писать тесты такого типа.

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

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

Небольшая заметка о насмешках

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

Вы слышали о библиотеке тестирования React?

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

Его создатель, Кент С. Доддс, писал это с учетом этого:

«Чем больше ваши тесты будут похожи на то, как используется ваше программное обеспечение, тем больше уверенности они могут вам дать».

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

Для начала нам понадобится приложение

Я написал небольшое приложение, состоящее из двух страниц. На домашней странице вы можете написать postId. Кнопка Submit отключена, пока что-нибудь не напишешь.

После того, как вы напишете postId и нажмете кнопку Submit, вы перейдете на вторую страницу, /post/:postId.

Когда вы попадете на эту страницу, вы сначала увидите Loading... сообщение:

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

Вот полный код:

import React from "react";
import { BrowserRouter as Router, Route, Link, Switch } from "react-router-dom";
import { fetchPost } from "./api";

export default function App() {
  return (
    <Router>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route exact path="/post/:postId" component={Post} />
      </Switch>
    </Router>
  );
}

function Home({ history }) {
  const [postId, setPostId] = React.useState("");
  return (
    <div>
      <h1>Welcome!</h1>
      <h2>Search for a post by its ID</h2>

      <label htmlFor="postId">Post ID: </label>
      <input
        id="postId"
        value={postId}
        onChange={e => setPostId(e.target.value)}
      />
      <button
        disabled={!postId}
        onClick={() => history.push(`/post/${postId}`)}
      >
        Submit
      </button>
    </div>
  );
}

function Post({ match }) {
  const { postId } = match.params;
  const [post, setPost] = React.useState();
  React.useEffect(() => {
    (async function fetch() {
      setPost(await fetchPost(postId));
    })();
  }, [postId]);
  return (
    <div>
      <h1>Post {postId}</h1>
      {!post ? (
        <p>Loading...</p>
      ) : (
        <>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </>
      )}
      <Link to="/">Back to Home</Link>
    </div>
  );
}

А это файл api.js:

export const fetchPost = async postId => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${postId}`
  );
  return response.json();
};

Вы можете поиграть с приложением в песочнице.

Теперь мы готовы к тесту!

Я не буду писать о конфигурациях в этом посте. Я предполагаю, что у вас настроена библиотека Jest и React Testing Library, и вы готовы написать свой тест.

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

Начнем с импорта. Конечно, нам нужно сначала импортировать React, а также нам нужны render и screen из библиотеки тестирования React:

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

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

Теперь нам нужно создать наш тест:

test("Can search for a post using its ID", async () => {});

Мы импортируем наш App компонент и вызываем функцию render.

import App from "../app";

test("Can search for a post using its ID", async () => {
  render(<App />);
});

Большой! Наш тест должен быть успешным. Теперь мы можем начать использовать screen, чтобы проверить, отображает ли наш компонент то, что он должен.

Домашняя страница

Мы находимся на главной странице, поэтому должны увидеть сообщение Добро пожаловать. Поскольку это текст, мы можем использовать getByText запрос. Мы получаем этот элемент и затем заключаем его в выражение expect, ожидая, что он существует в документе.

expect(screen.getByText(/welcome/i)).toBeInTheDocument();

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

Давайте посмотрим на пример. На нашей домашней странице у нас есть элемент h2, который говорит Search for a post by its ID. Мы вполне могли бы это сделать, и это сработает:

expect(screen.getByText("Search for a post by its ID")).toBeInTheDocument();

Но что, если на следующей неделе мы изменим эту фразу на Here you can search for a post. The only thing you need is its ID? Конечно, теперь наш тест не работает! Лучше всего написать это утверждение так:

expect(screen.getByText(/search.*post.*id/i)).toBeInTheDocument();

Это идеально! Мы знаем, что у нас есть три важных слова, которые всегда будут там (search, post и id). С этим утверждением тест не сломался бы, если бы мы изменили нашу фразу, как мы сказали ранее.

Последнее, что нам нужно проверить на нашей домашней странице, - это кнопка Submit, которая должна быть там, но должна быть отключена.

expect(screen.getByText(/submit/i)).toBeDisabled();

Поиск сообщения

Наша домашняя страница отображается правильно и имеет все необходимое для поиска сообщения. Нам нужно имитировать ввод текста пользователем в нашем поле input, и у библиотеки тестирования React есть наши спины.

Нам нужно импортировать модуль user-event:

import user from "@testing-library/user-event";

Но прежде чем мы сможем смоделировать ввод пользователя в поле input, нам нужно получить этот элемент. При тестировании форм рекомендуется получать элементы по их label. Таким образом мы также можем проверить, правильно ли связаны поля label и input, что важно для доступности.

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

screen.getByLabelText(/post id/i);

И теперь мы готовы смоделировать ввод пользователя с помощью модуля user-event:

user.type(screen.getByLabelText(/post id/i), "1");

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

const submitButton = screen.getByText(/submit/i);
expect(submitButton).toBeEnabled();
user.click(submitButton);

Посадка на страницу поста

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

screen.getByText(/loading/i);

Но если вы это напишете, то увидите, что тест не пройдет:

Когда это происходит, нам нужно использовать find* запросы вместе с await. Затем тест будет ждать, пока не появится сообщение о загрузке.

await screen.findByText(/loading/i);

Идеально! Сейчас тест проходит.

Мокинг HTTP-запросов

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

Прежде всего, сразу после импорта давайте смоделируем модуль api, используя jest:

jest.mock("../api");

И теперь мы можем импортировать модуль как его имитируемую версию:

import { fetchPost as mockFetchPost } from "../api";

В нашем тесте давайте создадим фиктивную запись, объект, который вернет наш фальшивый запрос при разрешении:

const mockPost = {
  id: "1",
  title: "Post Title",
  body: "Post Body",
};

А затем проинструктируем нашу фиктивную функцию вернуть этот объект при вызове:

mockFetchPost.mockResolvedValueOnce(mockPost);

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

user.type(screen.getByLabelText(/post id/i), mockPost.id);

Идеально! Все настроено, и теперь мы можем продолжить наш тест.

Завершение нашего теста

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

expect(mockFetchPost).toHaveBeenCalledTimes(1);

А также то, что он вызывается с помощью определенного нами фиктивного идентификатора сообщения:

expect(mockFetchPost).toHaveBeenCalledWith(mockPost.id);

Теперь давайте проверим, что актуальная информация о публикации отображается на экране для пользователя:

expect(screen.getByText(mockPost.title)).toBeInTheDocument();
expect(screen.getByText(mockPost.body)).toBeInTheDocument();

Осталось проверить только обратную ссылку. Сначала щелкаем по нему:

user.click(screen.getByText(/back.*home/i));

И тогда мы должны оказаться на главной странице:

await screen.findByText(/welcome/i);

Были сделаны! Это полный тест:

import React from "react";
import { render, screen } from "@testing-library/react";
import user from "@testing-library/user-event";
import { fetchPost as mockFetchPost } from "../api";
import App from "../app";

jest.mock("../api");

test("Can search for a post using its ID", async () => {
  const mockPost = {
    id: "1",
    title: "Post Title",
    body: "Post Body",
  };
  mockFetchPost.mockResolvedValueOnce(mockPost);
  render(<App />);

  expect(screen.getByText(/submit/i)).toBeDisabled();
  expect(screen.getByText(/welcome/i)).toBeInTheDocument();
  expect(screen.getByText(/search.*post.*id/i)).toBeInTheDocument();

  user.type(screen.getByLabelText(/post id/i), mockPost.id);
  const submitButton = screen.getByText(/submit/i);
  expect(submitButton).toBeEnabled();
  user.click(submitButton);

  await screen.findByText(/loading/i);
  expect(mockFetchPost).toHaveBeenCalledWith(mockPost.id);
  expect(mockFetchPost).toHaveBeenCalledTimes(1);
  expect(screen.getByText(mockPost.title)).toBeInTheDocument();
  expect(screen.getByText(mockPost.body)).toBeInTheDocument();

  user.click(screen.getByText(/back.*home/i));
  await screen.findByText(/welcome/i);
});

Вот и все!

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

Конечно, есть еще что проверить, даже в этом небольшом приложении. Я надеюсь, что это было полезно и что вы начнете писать много длинных интеграционных тестов для своих приложений 🚀