Команда 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