Команда ReactJS выпустила Hooks API в версии 16.8. В сопроводительном сообщении в блоге react-testing-library получил от них официальную поддержку. Они выделяют библиотеку за то, как она упрощает тестирование. Я очень рад это видеть. Я хотел рассказать, почему эта библиотека прекрасна и как она поможет вам стать лучшим фронтенд-разработчиком.

В этом посте мы исследуем философию важности тестирования. Цель состоит в том, чтобы поделиться некоторыми практическими советами о том, как использовать react-testing-library, которые вы можете использовать сегодня.

Зачем тестировать? 🤔

Как инженеры-программисты, мы работаем над интересными областями. В моем случае я работаю в VSware, помогая директорам и учителям упростить повседневную работу. Это потрясающе! Но я склонен бороться с тем фактом, что в этом так много всего. Работаем над существующими функциями. Исправление ошибок и недочетов. Реализация новых требований. Обход и понимание устаревших кодовых баз. Попытка сохранить хорошие, чистые инженерные практики. Я нахожу это подавляющим. Сложность мучительна. 🤯

Это иррациональное поведение. Я увяз в этой сложности. Но один способ, который помогал мне не волноваться, - это серьезно относиться к своим дисциплинам тестирования. Тестирование вселяет уверенность!

За свою карьеру я работал с компаниями, которые сосредоточены на выпуске продукта. Потребность в уверенности исходит из моего опыта. Они сосредоточены на одном: на предоставлении ценности своим клиентам. Меня наняли, чтобы облегчить чью-то жизнь. Чтобы обеспечить эту ценность. У них есть набор дел, которые им нужно делать каждый день, и важно, чтобы они могли делать свою работу. Их не волнует новейшая оболочка интерфейса или количество хуков, которые вы можете использовать, они хотят выполнять свою работу. Частично эта уверенность возникает из-за прохождения набора «зеленых» тестов.

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

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

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

Что я тестирую? 🤷‍♀️

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

Как протестировать 💡

Здесь мы входим в библиотеку тестирования-реакции. Написанная Кентом С. Доддсом, это библиотека, которая помогает сфокусировать ваши тесты на поведении пользователей. Он предоставляет хороший набор API, которые помогают упростить задачу. Легко захватить элементы со страницы / компонента. Этими элементами также легко манипулировать, готовые к утверждениям с помощью таких инструментов, как Jest.

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

Сделайте ваши тесты устойчивыми к изменениям 💃

Стоит отметить: многие из этих идей полностью подтверждены замечательной статьей Кента С. Доддса. Я воплощаю в жизнь эту уже хорошо зарекомендовавшую себя идею!

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

Возьмем простой пользовательский <Input /> компонент. Небольшая обертка вокруг исходного input с необязательным label:

export function Input({ label, id, ...props }) {
  return (
    <>
      {label && (
        <label
          className="myFancyLabelStyles"
          htmlFor={id}
        >
          {label}
        </label>
      )}
      <input
        className="myFancyInputStyles"
        id={id}
        {...props}
      />
    </>
  );
}

Прежде всего, ваша немедленная инстинктивная реакция была бы такой: зачем мне вообще это проверять? Это слишком просто. Это может быть правдой в зависимости от вашего варианта использования! Допустим, этот <Input /> появляется по всему вашему продукту. Все формы и взаимодействия с пользователем используют его. Можно убедиться, что он имеет стандартное поведение обычного тега HTML input. Это сделано для того, чтобы в вашем продукте не было поломок, если что-то плохое попадет в этот компонент.

Наличие хорошего селектора для вашего компонента может помочь сделать ваш тест устойчивым. Давайте рассмотрим преимущества и недостатки некоторых из них:

By class/className eg..myFancyInputStyles

Классы - это забота о стилизации. Я не согласен с их использованием в вашем тесте. Вы должны иметь возможность изменять CSS без того, чтобы ваш тест испортил желание использовать другой класс / набор стилей. Что, если я хочу заменить свой стиль ввода CSS-фреймворком Tailwind? Или styled-components? Избегайте их использования в тесте.

По тегу HTML, например. input 🤷‍♂️

Я бы также избегал этого, если бы не знал наверняка, что у меня будет только один вход в моем компоненте, который я тестирую. Иначе могли бы быть танцы, пытающиеся организовать различные input элементы. Было бы беспорядком пытаться отличить их друг от друга.

По лейблу aria, например. [aria-label="username"] 👍

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

По идентификатору теста, например. [data-testid="username"] 🙌

Используя идентификатор теста, мы делаем связь между тестом и частью компонента явной. react-testing-library предоставляет несколько полезных помощников для работы с компонентами, написанными таким образом.

По значению метки / заполнителя, например. /username/i 🤗

Для меня это один из самых интуитивно понятных методов, который помог мне читать и писать тесты по-человечески. Это то, что заставило меня полностью влюбиться в react-testing-library.

Вот несколько примеров тестирования нашего <Input/> выше:

import 'react-testing-library/cleanup-after-each'
import React from 'react'
import { Input } from './index'
import { render, fireEvent } from 'react-testing-library'
it('Able to get input by placeholder', () => {
  const { getByPlaceholderText } = render(<Input placeholder={'Name'} />)
  const input = getByPlaceholderText('Name')
  fireEvent.change(input, { target: { value: 'John' } })
  expect(input.value).toBe('John')
})
it('Able to get by test id', () => {
  const { getByTestId } = render(<Input data-testid="username" />)
  const input = getByTestId('username')
  fireEvent.change(input, { target: { value: 'johnb' } })
  expect(input.value).toBe('johnb')
})
it('Able to get input by label', () => {
  const { getByLabelText, debug } = render(
    <Input label={'Surname'} id="surname" />
  )
  const input = getByLabelText(/surname/i)
  debug(input)
  fireEvent.change(input, { target: { value: 'Brennan' } })
  expect(input.value).toBe('Brennan')
})

Мощь исходит от функции render, которую он предоставляет в своем API верхнего уровня. Работает как рендер из react-dom. За исключением того, что он возвращает множество полезных функций запроса для получения частей компонента. getByPlaceHolderText, getByTestId и getByLabelText получают наш элемент input. react-testing-library предоставляет еще одну функцию верхнего уровня под названием fireEvent. Это может отправить событие компоненту, чтобы затем выполнить некоторые утверждения для элемента.

Еще одна интересная функция, которую я использовал в прошлом тесте, - это debug, которая также возвращается из функции render. Часто мне сложно отлаживать тесты при использовании Jest. Может быть трудно понять, что происходит в компоненте, не имея возможности увидеть это в браузере. debug красиво печатает ваш компонент. Он может принимать необязательный параметр для печати только HTML, в зависимости от того, какой элемент вы передаете функции.

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

Структурирование ваших тестов для успеха 💪

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

Глобальная функция рендеринга 🌎

Типичное приложение React вызывает множество глобальных проблем. Глобальное управление состоянием с Redux. Маршрутизация с помощью чего-то вроде Reach Router или React Router. Аутентификация. Интернационализация. Я мог бы продолжить. Когда я только начинал, мне было сложно совмещать эти проблемы с моими тестами. Приходится пробовать издеваться над вещами, заканчивая уродливым шаблоном. Это расстраивает: это детали реализации, и я не хочу о них беспокоиться.

Светит react-testing-library - это сила функции рендеринга. Вы можете обернуть глобальные проблемы, расширив функциональность функции рендеринга. Давайте рассмотрим пример, чтобы проиллюстрировать это:

import { render as r } from 'react-testing-library'
import { createStore } from 'redux'
import { Provider as ReduxProvider } from 'react-redux'
import { LocationProvider, createHistory } from "@reach/router"
import { AuthenicationProvider, createAuth } from '../auth'
import { reducer } from '../state'
export function reducer(ui, {
  initialState = {},
  store = createStore(reducer, initialState),
  history = createHistory(),
  auth = createAuth()
} = {}) {
  const WrapperUI = () => (
    <ReduxProvider store={store}>
      <AuthenicationProvider auth={auth}>
         <LocationProvider history={history}>{ui}</LocationProvider
      </AuthenicationProvider>
    </ReduxProvider>
  )
  return { ...r(<WrappedUI />), store, history }
}

Здесь у меня есть redux, @reach/router и вымышленный пользовательский модуль аутентификации. В файле тестовой утилиты я использую функцию рендеринга по умолчанию и назначаю ей псевдоним r. Затем я экспортирую свою собственную функцию рендеринга, которая знает все глобальные зависимости. Я также использую список параметров, для которых есть несколько разумных значений по умолчанию. Но вы можете переопределить в зависимости от конкретного сценария использования теста.

Допустим, вы хотите протестировать какой-то не начальный шаг в пользовательском потоке. Опция отправляет некоторую исходную информацию в хранилище redux. Пример, в котором я хочу передать начальный путь, поэтому я могу сделать некоторые утверждения, подтверждающие переход по правильному URL-адресу. Здесь может пригодиться возвращенный объект history.

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

Специальная функция рендеринга для теста

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

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

Для примера <LoginForm /> компонента мы могли бы иметь такую ​​структуру в тесте:

import { render as r } from '../test-utils'
function render(ui, options) {
 const utils = r(ui, options)
 return {
   ...utils,
   username: utils.getByLabelText(/username/i),
   password: utils.getByLabelText(/password/i),
   login: utils.getByText(/login/i),
   successModal: () => utils.getByTestId('login-success')
  }
}

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

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

it('Should let me login given a username and password', () => {
  const auth = { login: jest.fn() }
  const { username, password, login } = render(<Login /> { auth })
  
  fireEvent.change(username, { target: { value: 'john' } })
  fireEvent.change(password, { target: { value: 'sekret' } })
  fireEvent.click(login)
  expect(auth.login).toHaveBeenCalledWith('john', 'sekret')
})

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

Призыв к действию 📣

Все эти идеи я узнал из классного курса testingjavascript.com. Я рекомендую вам попросить вашего менеджера получить этот курс для вас. Или купите, если у вас есть такая возможность! Это было полезно для меня и помогло мне щелкнуть при тестировании пользовательского интерфейса. Вы также можете перейти на testing-library.com, чтобы узнать больше о библиотеке и ее удивительных простых API.

Заключение 🔚

Идеи, представленные в этой статье, не новаторские. Они отлично подходят для темы, которую я когда-то долгое время считал скучной. Я не могу порекомендовать вам попробовать эту библиотеку. Это было очень полезно для меня, когда я действительно хотел писать тесты как часть моей работы с функциями. Шаблоны, которые я обсуждал, снова и снова приносили мне победы, и работать с ними я чувствую, как мечту. Это вселило в меня уверенность, о которой я упоминал в начале поста. Мне удобнее отправлять свои функции в производство.

Я рассказал все о содержании этого поста на встрече ReactJS в Дублине в январе 2019 года. Если вам нужна видеоверсия представленных идей, посмотрите ее!

Если у вас есть какие-либо вопросы или мысли о тестировании в интерфейсе, я хотел бы их услышать! Поймай меня в твиттере https://twitter.com/jgbrenno