Сделайте ваши тесты снова надежными

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

Детали реализации тестирования

Приходилось ли вам когда-нибудь исправлять свои тесты из-за того, что вы обновляли имя ключа состояния в своем приложении React? Как насчет того, чтобы удалить <div>с id, который искали ваши тесты? Считаете ли вы, что ваши тесты настолько хрупкие, что любые обновления кода могут привести к их сбою?

Не волнуйся, ты не единственный. Бесчисленные разработчики (в том числе и я) попадают в ловушку деталей тестирования реализации вместо реального взаимодействия с пользователем. Однако мы не единственные виновные стороны. Доступные библиотеки тестирования, такие как Enzyme, предоставляют API-интерфейсы, позволяющие напрямую изменять состояние приложения, что побуждает к тестированию деталей реализации. Например, используя примеры из setState API:

// Foo.js
class Foo extends React.Component {
  constructor(props) {
    super(props);
    this.state = { name: 'foo' };
  }
render() {
    const { name } = this.state;
    return (
      <div className={name} />
    );
  }
}
// Foo.test.js
const wrapper = shallow(<Foo />);
expect(wrapper.find('.foo')).to.have.lengthOf(1);
expect(wrapper.find('.bar')).to.have.lengthOf(0);
wrapper.setState({ name: 'bar' });
expect(wrapper.find('.foo')).to.have.lengthOf(0);
expect(wrapper.find('.bar')).to.have.lengthOf(1);

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

Почему библиотека тестирования React?

React Testing Library - это библиотека тестирования, разработанная мастером React Kent C. Dodds и многочисленными уважаемыми разработчиками со всего мира. На мой взгляд, RTL лучше других библиотек тестирования, потому что он

  • Предотвращает детали реализации тестирования
  • Имитирует реальное взаимодействие пользователя с компонентами
  • Самостоятельные документы
  • Предоставляет отличные инструменты, упрощающие написание тестов.

Теперь позвольте мне глубже погрузиться в эти преимущества и, надеюсь, убедить вас сесть на поезд RTL.

Использование библиотеки тестирования React

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

У нас есть простой счетчик, который может увеличивать или уменьшать отображаемое количество.

import React, { useState } from 'react';
const Counter = () => {
  const [count, setCount] = useState(0);
  const increment = () => setCount(count => count + 1);
  const decrement = () => setCount(count => count - 1);
  return (
    <div>
      <div>Count: {count}</div>
      <button onClick={decrement}>Decrement</button>
      <button onClick={increment}>Increment</button>
    </div>
  );
};
export default Counter;

С RTL ваш тест может выглядеть так

import * as React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './counter';
test('should update count when button clicked', () => {
  const { container } = render(<Counter />);
  const increment = screen.getByRole('button', {name: /increment/i});
  const decrement = screen.getByRole('button', {name: /decrement/i});
  const message = screen.getByText(/count/i);
  
  expect(message).toHaveTextContent('Count: 0');
  
  userEvent.click(increment);
  expect(message).toHaveTextContent('Count: 1');
  
  userEvent.click(decrement);
  expect(message).toHaveTextContent('Count: 0');
});

Из этого примера мы видим, что мы не манипулируем напрямую каким-либо состоянием приложения; Скорее, мы имитируем то, как пользователь будет взаимодействовать с приложением. Делая это, мы можем свободно обновлять компонент <Counter /> и даже добавлять новые функции, не беспокоясь о сбоях тестов. Эти тесты останутся действительными до тех пор, пока мы не изменим поведение тестируемых функций. Кроме того, тесты, подобные приведенным выше, настолько ясны и понятны, что могут легко служить документацией для ваших компонентов.

💡 Когда я просматриваю код, я всегда просматриваю тесты перед реальным кодом, чтобы понять ожидаемое поведение. Подобные тесты определяют четкие спецификации того, что должен делать компонент.

userEvent

Вы можете заметить, что мы используем userEvent API от @testing-library/user-event. Это не обычная MouseEvent('click') функция. Наши старые способы создания MouseEvents для взаимодействия с элементом также были формой тестирования деталей реализации. Отправка события click вручную не проверяет, является ли элемент интерактивным или интерактивным, что может скрыть смертельную ошибку в вашем рабочем коде. userEvent точно имитирует реальное взаимодействие с пользователем и предупредит вас, если элемент недоступен для клика. Если этого мало, существует целый список библиотек в экосистеме RTL.

Инструменты разработчика

Начинаете видеть его ценности?

screen.debug() & screen.logTestingPlaygroundURL()

Иногда бывает сложно найти правильный элемент, который вы хотите протестировать, и это определенно сбивает меня с толку, когда запрос, в котором я так уверен, возвращает null или undefined. screen API от @testing-library/dom имеет две функции, которые устранят двусмысленность вокруг этого. Добавив screen.debug() в свой тест, когда тест будет запущен, вы сможете увидеть дерево DOM для визуализированного компонента, например:

Это позволит вам точно определить правильный запрос для утверждения в ваших тестах. Если это все еще недостаточно очевидно, вы можете использовать screen.logTestingPlaygroundURL(), чтобы понять это за вас. Эта команда вернет сгенерированную ссылку, по которой вы можете перейти в своем браузере:

Это подводит нас к следующему и последнему пункту продажи библиотеки тестирования React.

Тестовая площадка

Несмотря на то, что нам, разработчикам, нравится программировать, иногда приятно отдохнуть от утомительной работы и позволить другим инструментам сделать за нас часть тяжелой работы. Именно здесь на сцену выходит площадка для тестирования. Площадка для тестирования - это интерактивный сайт, на котором вы можете вставить дерево DOM и найти точный запрос для элемента, который вам нужен. Вы можете либо вставить результаты из screen.debug(), либо перейти по ссылке вывода из screen.logTestingPlaygroundURL(), чтобы начать работу, и выбрать элемент, который вы хотите запросить, скопировать и вставить результат прямо в свои тесты. Пользовательский интерфейс похож на проверку элемента с помощью инструмента разработчика, поэтому нет необходимости в обучении, чтобы начать использовать сайт в полном объеме. Чтобы сделать его еще более удобным, есть также расширение для браузера, которое позволяет вам взаимодействовать с любым сайтом!

Заключение

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

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

Больше контента на plainenglish.io