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

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

При хорошем проценте охвата у всех меньше страха перед изменением кода, и, следовательно, это приводит к большему счастью.

Первый вопрос, который мы задаем себе, когда начинаем писать модульные тесты: «Что мне тестировать?». Ошибочный ответ на этот вопрос может привести к слишком большому или слишком малому количеству тестов. Нам нужна только нужная сумма!

Начнем с основ.

Модульный тест

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

Что мы должны тестировать в этой функции?

function sum (a, b) {
  return a + b
}
  • Когда мы вызываем функцию «сумма» с двумя числами (входами), мы ожидаем получить конкретный результат (выход).

Компоненты клиентского интерфейса для модульного тестирования

Для компонентов внешнего интерфейса концепция такая же, но вместо функций наша единица - это компонент.

Отличное объяснение можно найти в документации Vue Test Utils:

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

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

Например, для компонента Counter, который увеличивает счетчик отображения на 1 при каждом нажатии кнопки, его тестовый пример будет имитировать щелчок и утверждать, что отображаемый результат увеличился на 1. Тест не t заботится о том, как Counter увеличивает значение, он заботится только о вводе и выводе.

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

Также важно подчеркнуть, что:

  • Каждая функция тестирования должна проверять отдельную концепцию. Описания тестов должны быть краткими, точными и точно отражать то, что мы тестируем. Нам не нужно читать его реализацию, чтобы понять его;
  • По возможности мы должны имитировать зависимости компонентов. В центре нашего внимания находится компонент, его зависимости (дочерние компоненты, хранилище, маршрутизатор, вызовы / ответы API и даже сторонние библиотеки) должны иметь свои собственные тесты. Имитация зависимостей заставит наши тесты работать быстрее и позволит нам избежать их повторного тестирования. Это также даст нам больше контроля над входами компонентов;
  • Мы должны доверять фреймворку, а не тестировать его;
  • Очень важно проверять сценарии ошибок. В наших приложениях многое может пойти не так, но некоторые вещи, например, запросы API, возвращающие ошибки, мы можем предсказать. Мы должны проверить, что наше приложение в таких случаях реагирует ожидаемым образом;
  • Используйте тестовое покрытие в качестве помощника, чтобы определить, не забыли ли мы что-то протестировать, но не как источник истины, что все было протестировано. Вы можете получить 100% покрытие без тестирования всего, что вам нужно!

Входы и выходы

Входы - это все, что может привести к побочному эффекту (выходу) компонента.

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

По мере того, как мы увеличиваем сложность компонента, начинают появляться другие типы входных данных. Данные, полученные из хранилища (Redux, Vuex), ответы API или информация о маршрутизаторе - очень распространенные.

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

Все эти входные данные вызывают некоторый побочный эффект на наш компонент. Это может быть отрисовка метки (DOM), отправляемое событие, вызываемый магазин или API, отправка данных в дочерний компонент или даже изменение нашего URL-адреса (Route).

Документация

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

Чтобы избежать этого беспорядка, необходима хорошая структура:

Следуя такой стратегии, описания тестов становятся короче, и становится легче найти конкретный тест или проверить, что еще нужно протестировать.

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

Практический пример

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

В этом примере у нас есть два компонента: UsersList и UsersListRow. Чтобы проверить их, мы задаем вопрос: что на самом деле делает каждый компонент? Давайте выделим каждую визуально, чтобы быстро ответить.

UsersListRow

Что оно делает?

  • Получает и отображает опору «имя»;
  • Когда мы нажимаем на него, генерирует событие «view» с опорой «id» в качестве полезной нагрузки;
  • Когда мы щелкаем по значку редактирования, генерирует событие «edit» с опорой «id» в качестве полезной нагрузки;
  • Когда мы щелкаем по значку удаления, генерирует событие «удалить» с опорой «id» в качестве полезной нагрузки;

UsersList

На следующем изображении содержимое UsersListRow пусто, потому что это не имеет значения. Нам нужно сосредоточиться на том, что делает компонент UsersList, и для этого нам нужно знать только общедоступный API компонента UsersListRow.

Что оно делает?

  • Получает список из магазина и отображает соответствующее количество UsersListRow.
  • Получает общее количество элементов из магазина и отображает кнопку «загрузить больше», когда количество отображаемых элементов меньше указанного;
  • Вызывает магазин, чтобы получить больше товаров, когда мы нажимаем кнопку «Загрузить еще»;
  • Открывает страницу сведений, когда UsersListRow генерирует событие «просмотр»;
  • Открывает страницу редактирования, когда UsersListRow генерирует событие «edit»;
  • Вызывает магазин для удаления элемента, когда UsersListRow генерирует событие «удалить»;

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

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

Примечание

Изменение UsersListRow на генерирование события «remove» вместо события «delete» приведет к нарушению модульных тестов UsersListRow, но не тестов UsersList. За выявление этой проблемы отвечают интеграционные / функциональные тесты.

Заключение

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

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

Чтобы ответить на вопрос «Что мне тестировать?» мы должны быть в состоянии ответить на вопрос «Что делает компонент?».

Спасибо за прочтение. Вы можете узнать больше обо мне на Medium, Github и Linkedin.