Советы и рекомендации по поддержанию чистоты ваших тестовых наборов

Модульные тесты важны. Они предотвращают регрессию при рефакторинге кода, служат в качестве документации и экономят часы, которые вы не тратите на утомительное ручное тестирование. Короче говоря, тесты позволяют изменить.

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

Взгляните на эту цитату Роберта К. Мартина:

«Тестовый код так же важен, как и производственный. Это не гражданин второго сорта. Это требует мысли, дизайна и заботы. Он должен быть таким же чистым, как производственный код ».

Итак, как нам сохранить наш тестовый код в чистоте? Давайте рассмотрим некоторые идеи ниже.

Структурирование тестов

Тесты должны быть структурированы в соответствии с шаблоном Arrange-Act-Assert. У этого шаблона много названий, и его иногда называют шаблоном Build-Operate-Check, Setup-Exercise-Verify или Given-When-Then.

Я предпочитаю Arrange-Act-Assert из-за заманчивой аллитерации. Независимо от того, как вы это называете, узор выглядит так:

  • Упорядочить: настройте тестовые приборы, объекты или компоненты, с которыми вы будете работать.
  • Действие. Выполните какое-либо действие, например, вызвав функцию или нажав кнопку.
  • Утверждение: утверждение, что произошло ожидаемое поведение или результат.

В мире React применение этого шаблона при тестировании простого компонента переключателя может выглядеть следующим образом:

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

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

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

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

Конструкторы тестовых объектов

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

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

Одна из библиотек, которую я считаю особенно полезной, - это пакет npm faker.js. Мы можем использовать этот пакет для создания фиктивных данных для самых разных полей, таких как firstName, jobTitle, phoneNumber и других.

Рассмотрим этот пример для User построителя тестовых объектов:

Наш метод buildUser возвращает простой объект, представляющий пользователя. Затем мы можем использовать этот метод buildUser в наших тестовых файлах для создания пользователей со случайными значениями по умолчанию (пользователь user1) или пользователей с указанными нами конкретными значениями (пользователь user2).

Оцените одну концепцию за тест

Каждый тест должен подтверждать только одно. Не пытайтесь протестировать несколько вещей в одном тесте. Например, плохой тест для компонента выбора даты может прочитать что-то вроде «отображает в различных состояниях», а затем отобразить восемь разных средств выбора даты, чтобы проиллюстрировать различия. Подобный тест делает слишком много.

Более точный тест был бы более конкретным - что-то вроде «отображает средство выбора даты, когда пользователь нажимает ввод текста».

Тесты должны быть быстрыми

Медленные тестовые наборы - это проблема. Хуже того, когда медленные наборы тестов являются необязательными или не применяются как часть конвейера CI, разработчики, как правило, предпочитают не запускать эти наборы тестов. Никто не любит ждать.

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

В мире JavaScript выполнение тестов Jest в watch режиме во время разработки меняет правила игры.

Тесты должны быть независимыми

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

Если вы используете Jest, установка и удаление обычно выполняется в beforeEach и afterEach блоках кода. Также полезно помнить, что каждый тестовый файл получает свой собственный экземпляр JSDOM, но тесты в одном и том же файловом ресурсе используют один и тот же JSDOM экземпляр.

Тесты должны быть повторяемыми

Тесты должны выполняться в любой среде. Если набор тестов проходит на моей машине, он должен пройти и на вашей машине. Это также означает, что он должен проходить в конвейере CI. Когда тесты повторяемы, нет ничего удивительного в том, что тест проходит в одной среде, но терпит неудачу в другой. Подобная нестабильность снижает вашу уверенность в своих тестах.

Тесты должны быть самоутверждающимися

Тесты должны возвращать логическое значение. Либо тест проходит, либо не проходит. Вам не нужен человек, чтобы интерпретировать результаты теста. Это одна из многих причин, по которым тесты снимков - отстой, и их следует избегать.

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

Тесты должны быть написаны своевременно

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

Убедитесь, что тесты терпят неудачу, когда они должны

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

Взгляните на эту цитату Мартина Фаулера:

«Мне нравится видеть, как тест терпит неудачу хотя бы раз, когда я его пишу».

Это мудрые слова! Легко убедиться, что ваш тест выполняет свою работу, внеся небольшие изменения либо в тестовый код, либо в производственный код, чтобы изменить вывод на что-то намеренно некорректное. Если ваш тест не удался, отлично!

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

Не забудьте проверить свои пограничные кейсы

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

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

Мы вызовем функцию triangleType, и у нее будет три параметра, так что сигнатура функции будет выглядеть так: triangleType(side1, side2, side3).

В каких случаях вы бы протестировали такую ​​функцию?

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

  1. triangleType(4, 4, 4) // Equilateral Triangle
  2. triangleType(6, 7, 6) // Isosceles Triangle
  3. triangleType(6, 7, 8) // Scalene Triangle

Интересно, что тестирование этих трех случаев даст вам 100% покрытие кода в зависимости от текущей реализации функции. Но одних этих трех испытаний недостаточно.

Что, если бы, например, функции были предоставлены все нули? Это не треугольник. В том-то и дело. Но функция идентифицировала бы это как равносторонний треугольник, поскольку все стороны равны.

Что, если бы функции были переданы отрицательные числа? У треугольника не может быть отрицательной длины. В этом нет никакого смысла.

А что, если две стороны были намного короче третьей? Тогда стороны не будут соединяться, и у нас не будет треугольника.

Эти три дополнительных тестовых примера могут выглядеть так:

  1. triangleType(0, 0, 0) // Not a triangle
  2. triangleType(-6, -7, -8) // Not a triangle
  3. triangleType(5, 3, 100) // Not a triangle

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

Проверьте, что вас больше всего беспокоит, чтобы пойти не так

Мне нравится стремиться к 100% тестированию, но важно не быть категоричным в отношении этого числа. Есть закон убывающей отдачи, и каждый дополнительный тест добавляет все меньше и меньше ценности. Если у вас 95% покрытия кода, возможно, не стоит получать последние 5% покрытия кода. Не все стоит тестировать.

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

Резюме

Ты сделал это! Если вам нужно быстро освежить в памяти все, что мы рассмотрели в этой статье, вот мои советы и рекомендации по модульному тестированию для чистого кода:

  1. Структурируйте свои тесты с помощью шаблона Arrange-Act-Assert.
  2. Используйте конструкторы тестовых объектов, чтобы упростить настройку тестов для часто используемых объектов.
  3. Оцените одну концепцию за тест.
  4. ПЕРВЫЙ - тесты должны быть быстрыми, независимыми, повторяемыми, с самопроверкой, и своевременно.
  5. Убедитесь, что тесты не проходят, когда должны.
  6. Помните свои границы и крайние случаи.
  7. Проверьте, что вас беспокоит больше всего.

Спасибо за чтение и удачного кодирования!