Базовый формат моков React компонентов

Автор оригинала: Daniel Irvine
  • Перевод

В преддверии старта курса «Автоматизация тестирования на JavaScript» продолжаем публиковать перевод серии полезных статей


В первой части этой серии статей мы рассмотрели, почему моки на самом деле полезны.

В этой части я расскажу о базовом формате маков React компонентов.

Все образцы кода для этой статьи доступны в этом репозитории.

dirv / mocking-react-components

Примеры мокинга React компонентов.

Давайте еще раз взглянем на компоненты, с которыми мы работаем: BlogPage и PostContent.

Вот BlogPage:

const getPostIdFromUrl = url =>
  url.substr(url.lastIndexOf("/") + 1)

export const BlogPage = ({ url }) => {

  const id = getPostIdFromUrl(url)

  return (
    <PostContent id={id} />
  )
}

BlogPage особо ничего не делает, кроме отображения PostContent. Но у него есть некоторая функциональность, которая нас интересует, а именно парсинг пропса url для получения id сообщения.

PostContent немного сложнее: он вызывает встроенную в браузер функцию fetch для получения текста сообщения в блоге по URL-адресу /post?id=${id}, где id - это переданный ему пропc.

export const PostContent = ({ id }) => {
  const [ text, setText ] = useState("")

  useEffect(() => {
    fetchPostContent(id)
  }, [id])

  const fetchPostContent = async () => {
    const result = await fetch(/post?id=${id})
    if (result.ok) {
      setText(await result.text())
    }
  }

  return <p>{text}</p>
}

На самом деле, то, что делает PostContent, не важно, потому что мы его больше не увидим!

Мы собираемся написать несколько тестов для BlogPage в нашем тестовом файле BlogPage.test.js. Для этого мы создадим мок PostContent, чтобы не беспокоиться о его реализации.

Важным моментом является то, что мы полностью заглушаем PostContent, чтобы наш набор тестов BlogPage.test.js был защищен от всего, что делает PostContent.

Вот мок для PostContent:

import { PostContent } from "../src/PostContent"

jest.mock("../src/PostContent", () => ({
  PostContent: jest.fn(() => (
    <div data-testid="PostContent" />
  ))
}))

Давайте разберем его.

  • Мок определяется с помощью jest.mock. Здесь должен быть отражен соответствующий импорт. Вызов подвешен, чтобы import можно было заменить. Jest заменяет весь модуль вашим ново определенным модулем. Итак, в этом случае мы мокаем весь ../src/PostContent файл.

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

  • Вызов jest.fn создает шпиона (spy): объект, который записывает, когда он был вызван и с какими параметрами. Затем мы можем проверить вызовы используя сопоставители toHaveBeenCalled и toHaveBeenCalledWith.

  • Параметр jest.fn определяет значение-заглушку, которое возвращается при вызове функции (при визуализации компонента).

  • Реализации заглушек всегда должны быть настолько простыми, насколько это возможно. Для React компонентов это подразумевает div - элемент HTML с, пожалуй, наименьшим количеством смысловой нагрузки!

  • У него есть атрибут data-testid, который мы будем использовать, чтобы получить конкретно этот элемент в DOM.

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

  • Значение data-testid совпадает с названием компонента. В данном случае это PostContent. Это стандартное соглашение, которому я следую для всех своих моков.

Это базовый формат моков React компонентов. 90% (или больше) моих моков выглядят именно так. У остальных 10% есть небольшие дополнения, которые мы рассмотрим в следующих статьях.

Теперь, когда у нас есть этот мок, давайте напишем несколько тестов для BlogPage.

Проверка того, что мок компонента отображается в DOM

describe("BlogPage", () => {
  it("renders a PostContent", () => {
    render(<BlogPage url="http://example.com/blog/my-web-page" />)
    expect(screen.queryByTestId("PostContent"))
      .toBeInTheDocument()
  })
})

Этот тест является первым из двух тестов, которые всегда требуются при использовании моков компонентов. screen.queryByTestId ищет в текущем DOM компонент со значением data-testid соответствующим PostContent.

Другими словами, он проверяет, действительно ли мы отрисовали PostContent компонент.

Ответственное использование queryByTestId

Обратите внимание, что я использовал queryByTestId. React Testing Library пытается оградить вас от этой функции на двух основаниях: во-первых, она хочет, чтобы вы использовали getBy вместо queryBy, во-вторых, как я уже упоминал выше, она не приветствует поиск по ID теста.

Фактически, тестирование моков - это единственный случай, когда я использую queryByTestId. Я не могу вспомнить случай, когда мне не удалось бы избежать использования TestId для не мокнутых компонентов. Но для моков это идеальный вариант: потому что это именно та техническая деталь, которую мы хотим проверить. Пользователь никогда не увидит этот компонент, он предназначен исключительно для наших тестов.

Мы получаем возможность иметь последовательный способ создания мок объектов: <div data-testid="ComponentName" /> - это стандартный шаблон, который мы можем использовать для всех мок объектов.

getBy* против queryBy*

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

Итак, если у вас есть:

expect(screen.getByTestId("PostContent"))
  .toBeInTheDocument()

Если бы вы не визуализировали <PostContent />, этот тест бы зафейлился с исключением из getByTestId. Ожидания никогда не сбываются!

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

Модульные тесты, и в частности тесты в стиле TDD (Разработка через тестирование), очень часто связаны с наличием элементов. Для этих тестов мне больше по душе queryBy.

Проверка того, что мок передал правильные пропсы

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

it("constructs a PostContent with an id prop created from the url", () => {
  const postId = "my-amazing-post"
  render(<BlogPage url={http://example.com/blog/${postId}} />)
  expect(PostContent).toHaveBeenCalledWith(
    { id: postId },
    expect.anything())
})

Здесь используются стандартные сопоставители Jest, toHaveBeenCalledWith, чтобы гарантировать, что функция PostContent была вызвана с параметрами, которые мы ожидали.

Когда React создает экземпляр вашего компонента, он просто вызывает определенную функцию с пропсами в качестве объекта в первом параметре и ref во втором параметре. Второй параметр обычно не важен.

Оператор JSX <PostContent id="my-amazing-post" /> приводит к вызову функции PostContent ({id: "my-amazing-post"}).

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

Использование expect.anything для второго параметра toHaveBeenCalledWith

Второй параметр, который React передает вашему компоненту, - это ссылка на экземпляр (ref). Обычно это неважно для наших тестов, поэтому вам всегда нужно передавать expect.anything(), чтобы указать, что вас не интересует его значение.

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

Если вы не передаете пропсы, просто используйте toHaveBeenCalled

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

Понимание основных правил мокинга компонентов

Мы написали два теста и один мок. Вот важные уроки, которые мы вынесли на данный момент:

  • Ваш мок должен быть шпионом с использованием jest.fn и иметь значение, возвращаемое заглушкой самого простого компонента, который у вас может быть, а именно <div />.

  • Вам также следует установить data-testid, чтобы вы могли напрямую определить этот элемент в DOM.

  • Значением этого атрибута по соглашению является имя мокаемого компонента. Итак, для компонента PostContent его заглушенное значение равно <div data-testid = "PostContent" />.

  • Каждый мок требует как минимум двух тестов: первый проверяет, присутствует ли он в DOM, а второй проверяет, что он был вызван с правильными пропсами.

Зачем нам эти два теста?

Я уже пару раз упоминал, что нам нужно как минимум два теста. Но почему?

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

export const BlogPost = () => {
  PostContent({ id: "my-awesome-post" })
  return null
}

Зачем вам это нужно - тема целой отдельной статьи. Вот краткая версия: обычно мы считаем, что вызов функции проще, чем JSX оператор. Когда вы используете строгие принципы тестирования, вы должны всегда писать самый простой код, чтобы тест был пройден.

А как насчет того, чтобы написать только первый тест, а на второй забить?

Вы можете сделать его проходимым так:

export const BlogPost = () => (
  <PostContent />
)

Опять же, это простейший производственный код для прохождения теста.

Чтобы перейти к фактическому решению, вам понадобятся оба теста.

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

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

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


Приглашаем вас записаться на бесплатный демо-урок по теме: "Основы puppeteer".

Читать ещё:

OTUS. Онлайн-образование
Цифровые навыки от ведущих экспертов

Похожие публикации

Комментарии 0

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

Самое читаемое