Перейти к основному содержимому

Общие рекомендации по модульному тестированию

Перевод

Перевод (gpt) и адаптация статьи Unit testing best practices with .NET Core and .NET Standard by John Reese

warning

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

Избегайте зависимостей от инфраструктуры

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

Название ваших тестов

Имя вашего теста должно состоять из трех частей:

  • Название тестируемого метода.
  • Сценарий, в котором он тестируется.
  • Ожидаемое поведение при вызове сценария.
Почему это важно?

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

Плохо
Процедура Тест_ОднаСтрока() Экспорт

Результат = ЮТСтроки.ДобавитьСтроку("Иванов", "");

ЮТест.ОжидаетЧто(Результат)
.Равно("Иванов");

КонецПроцедуры
Лучше
Процедура ДобавитьСтроку_БезДополнения_ВернетТужеСтроку() Экспорт

Результат = ЮТСтроки.ДобавитьСтроку("Иванов", "");

ЮТест.ОжидаетЧто(Результат)
.Равно("Иванов");

КонецПроцедуры

Организация ваших тестов

Подготовка, Действие, Проверка — это распространённый паттерн в юнит-тестировании. Как следует из названия, он состоит из трех основных действий:

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

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

Плохо
Процедура ДобавитьСтроку_БезДополнения_ВернетТужеСтроку() Экспорт

// Подготовка
ИсходнаяСтрока = "Иванов";
ПустаяСтрока = "";

// Проверка
ЮТест.ОжидаетЧто(ЮТСтроки.ДобавитьСтроку(ИсходнаяСтрока, ПустаяСтрока))
.Равно(ИсходнаяСтрока);

КонецПроцедуры
Лучше
Процедура ДобавитьСтроку_БезДополнения_ВернетТужеСтроку() Экспорт

// Подготовка
ИсходнаяСтрока = "Иванов";
ПустаяСтрока = "";

// Действие
Результат = ЮТСтроки.ДобавитьСтроку(ИсходнаяСтрока, ПустаяСтрока);

// Проверка
ЮТест.ОжидаетЧто(Результат)
.Равно(ИсходнаяСтрока);

КонецПроцедуры

Пишите минимально проходящие тесты

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

Почему это важно?
  • Тесты становятся более устойчивыми к будущим изменениям в кодовой базе.
  • Они ближе к тестированию поведения, а не реализации.

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

Плохо
Процедура ДобавитьСтроку_БезДополнения_ВернетТужеСтроку() Экспорт

// Подготовка
ИсходнаяСтрока = "Иванов";

// Действие
Результат = ЮТСтроки.ДобавитьСтроку(ИсходнаяСтрока, "", ";");

// Проверка
ЮТест.ОжидаетЧто(Результат)
.Равно(ИсходнаяСтрока);

КонецПроцедуры
Лучше
Процедура ДобавитьСтроку_БезДополнения_ВернетТужеСтроку() Экспорт

// Подготовка
ИсходнаяСтрока = "Иванов";

// Действие
Результат = ЮТСтроки.ДобавитьСтроку(ИсходнаяСтрока, "");

// Проверка
ЮТест.ОжидаетЧто(Результат)
.Равно(ИсходнаяСтрока);

КонецПроцедуры

Избегайте "магических" строк

Именование переменных в юнит-тестах так же важно, если не более важно, чем в рабочем коде. Юнит-тесты не должны содержать магических строк.

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

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

Совет

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

Плохо
Процедура ДобавитьСтроку_БезДополнения_ВернетТужеСтроку() Экспорт

Результат = ЮТСтроки.ДобавитьСтроку("Иванов", "");

ЮТест.ОжидаетЧто(Результат)
.Равно("Иванов");

КонецПроцедуры
Лучше
Процедура ДобавитьСтроку_БезДополнения_ВернетТужеСтроку() Экспорт

ИсходнаяСтрока = ЮТест.Данные().СлучайнаяСтрока();
ПустаяСтрока = "";

Результат = ЮТСтроки.ДобавитьСтроку(ИсходнаяСтрока, ПустаяСтрока);

ЮТест.ОжидаетЧто(Результат)
.Равно(ИсходнаяСтрока);

КонецПроцедуры

Избегайте логики в тестах

При написании юнит-тестов старайтесь избегать ручной конкатенации строк, логических условий (таких как Если, Пока, Для, #Если) и других условий.

Почему это важно?
  • Меньше шансов внедрить ошибку в тесты.
  • Фокус на конечном результате, а не на деталях реализации.

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

Совет

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

Плохо
Процедура ДобавитьСтроку() Экспорт

ИсходнаяСтрока = ЮТест.Данные().СлучайнаяСтрока();
ДополнительнаяСтрока = ЮТест.Данные().СлучайнаяСтрока();
РазделительПоУмолчанию = ";";

Варианты = ЮТест.Варианты("ИсходнаяСтрока, ДополнительнаяСтрока, Разделитель, Результат")
.Добавить(ИсходнаяСтрока, "", Неопределено, ИсходнаяСтрока)
.Добавить(ИсходнаяСтрока, ДополнительнаяСтрока, Неопределено, ИсходнаяСтрока + РазделительПоУмолчанию + ДополнительнаяСтрока)
.Добавить(ИсходнаяСтрока, ДополнительнаяСтрока, ";", ИсходнаяСтрока + ";" + ДополнительнаяСтрока)
.Добавить("", ДополнительнаяСтрока, Неопределено, ДополнительнаяСтрока)
;

Для Каждого Вариант Из Варианты.СписокВариантов() Цикл

Если Вариант.Разделитель = Неопределено Тогда
Результат = ЮТСтроки.ДобавитьСтроку(Вариант.ИсходнаяСтрока, Вариант.ДополнительнаяСтрока);
Иначе
Результат = ЮТСтроки.ДобавитьСтроку(Вариант.ИсходнаяСтрока, Вариант.ДополнительнаяСтрока, Вариант.Разделитель);
КонецЕсли;

ЮТест.ОжидаетЧто(Результат)
.Равно(Вариант.Результат);

КонецЦикла;

КонецПроцедуры
Лучше
Процедура ДобавитьСтроку_БезДополнения_ВернетТужеСтроку() Экспорт

ИсходнаяСтрока = ЮТест.Данные().СлучайнаяСтрока();

Результат = ЮТСтроки.ДобавитьСтроку(ИсходнаяСтрока, "");

ЮТест.ОжидаетЧто(Результат)
.Равно(ИсходнаяСтрока);

КонецПроцедуры

Процедура ДобавитьСтроку_БезИсходной_ВернетТужеСтроку() Экспорт

ДополнительнаяСтрока = ЮТест.Данные().СлучайнаяСтрока();

Результат = ЮТСтроки.ДобавитьСтроку("", ДополнительнаяСтрока);

ЮТест.ОжидаетЧто(Результат)
.Равно(ИсходнаяСтрока);

КонецПроцедуры

Процедура ДобавитьСтроку_СДополнением_ВернетОбъединениеСтрок() Экспорт

ИсходнаяСтрока = ЮТест.Данные().СлучайнаяСтрока();
ДополнительнаяСтрока = ЮТест.Данные().СлучайнаяСтрока();

Результат = ЮТСтроки.ДобавитьСтроку(ИсходнаяСтрока, ДополнительнаяСтрока);

ЮТест.ОжидаетЧто(Результат)
.НачинаетсяС(ИсходнаяСтрока)
.ЗаканчиваетсяНа(ДополнительнаяСтрока)
;

КонецПроцедуры

Процедура ДобавитьСтроку_СУказаннымРазделителем_ВернетОбъединениеСтрокИУказанногоРазделителя() Экспорт

ИсходнаяСтрока = ЮТест.Данные().СлучайнаяСтрока();
ДополнительнаяСтрока = ЮТест.Данные().СлучайнаяСтрока();
Разделитель = ЮТест.Данные().СлучайнаяСтрока();
ОжидаемыйРезультат = СтрШаблон("%1%2%3", ИсходнаяСтрока, ДополнительнаяСтрока, Разделитель)

Результат = ЮТСтроки.ДобавитьСтроку(ИсходнаяСтрока, ДополнительнаяСтрока, Разделитель);

ЮТест.ОжидаетЧто(Результат)
.НачинаетсяС(ИсходнаяСтрока)
.Содержит(Разделитель)
.ЗаканчиваетсяНа(ДополнительнаяСтрока)
.Равно(ОжидаемыйРезультат)
;

КонецПроцедуры

Предпочитайте вспомогательные методы для настройки и очистки

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

Почему это важно?
  • Меньше путаницы при чтении тестов, так как весь код виден в каждом тесте.
  • Снижается вероятность избыточной или недостаточной настройки для конкретного теста.
  • Уменьшается вероятность совместного использования состояния между тестами, что создает нежелательные зависимости.

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

Тоже самое касается методов ПередТестовымНабором и ПередВсемиТестами.

Плохо
Процедура ПередКаждымТестом() Экспорт

// Создание номенклатуры

// Установка цены

КонецПроцедуры

Процедура КонтрольОстатков_НаличиеОстатков_Успешно() Экспорт

// Алгоритм проверки

КонецПроцедуры

Процедура КонтрольОстатков_ОтсутствиеОстатков_Ошибка() Экспорт

// Алгоритм проверки

КонецПроцедуры

Процедура ПроверкаЗаполнения_НетЦены_Ошибка() Экспорт

// Алгоритм проверки

КонецПроцедуры
Лучше
Процедура КонтрольОстатков_НаличиеОстатков_Успешно() Экспорт

Номенклатура = НоваяНоменклатура();
УстановитьЦенуНоменклатуры(Номенклатура);

// Алгоритм проверки

КонецПроцедуры

Процедура КонтрольОстатков_ОтсутствиеОстатков_Ошибка() Экспорт

Номенклатура = НоваяНоменклатура();
УстановитьЦенуНоменклатуры(Номенклатура);

// Алгоритм проверки

КонецПроцедуры

Процедура ПроверкаЗаполнения_НетЦены_Ошибка() Экспорт

Номенклатура = НоваяНоменклатура();
// Алгоритм проверки

КонецПроцедуры

Функция НоваяНоменклатура() Экспорт
// Алгоритм создания
КонецФункции

Процедура УстановитьЦенуНоменклатуры(Номенклатура, Цена = 10) Экспорт
// Алгоритм установки
КонецПроцедуры

Избегайте нескольких действий

При написании тестов старайтесь включать только одно действие на тест. Распространенные подходы для этого:

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

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

Плохо
Процедура ДобавитьСтроку() Экспорт

ИсходнаяСтрока = ЮТест.Данные().СлучайнаяСтрока();
ДополнительнаяСтрока = ЮТест.Данные().СлучайнаяСтрока();
РазделительПоУмолчанию = ";";

Варианты = ЮТест.Варианты("ИсходнаяСтрока, ДополнительнаяСтрока, Разделитель, Результат")
.Добавить(ИсходнаяСтрока, "", Неопределено, ИсходнаяСтрока)
.Добавить(ИсходнаяСтрока, ДополнительнаяСтрока, Неопределено, ИсходнаяСтрока + РазделительПоУмолчанию + ДополнительнаяСтрока)
.Добавить(ИсходнаяСтрока, ДополнительнаяСтрока, ";", ИсходнаяСтрока + ";" + ДополнительнаяСтрока)
.Добавить("", ДополнительнаяСтрока, Неопределено, ДополнительнаяСтрока)
;

Для Каждого Вариант Из Варианты.СписокВариантов() Цикл

Если Вариант.Разделитель = Неопределено Тогда
Результат = ЮТСтроки.ДобавитьСтроку(Вариант.ИсходнаяСтрока, Вариант.ДополнительнаяСтрока);
Иначе
Результат = ЮТСтроки.ДобавитьСтроку(Вариант.ИсходнаяСтрока, Вариант.ДополнительнаяСтрока, Вариант.Разделитель);
КонецЕсли;

ЮТест.ОжидаетЧто(Результат)
.Равно(Вариант.Результат);

КонецЦикла;

КонецПроцедуры
Лучше
Процедура ДобавитьСтроку_БезДополнения_ВернетТужеСтроку() Экспорт

ИсходнаяСтрока = ЮТест.Данные().СлучайнаяСтрока();

Результат = ЮТСтроки.ДобавитьСтроку(ИсходнаяСтрока, "");

ЮТест.ОжидаетЧто(Результат)
.Равно(ИсходнаяСтрока);

КонецПроцедуры

Процедура ДобавитьСтроку_БезИсходной_ВернетТужеСтроку() Экспорт

ДополнительнаяСтрока = ЮТест.Данные().СлучайнаяСтрока();

Результат = ЮТСтроки.ДобавитьСтроку("", ДополнительнаяСтрока);

ЮТест.ОжидаетЧто(Результат)
.Равно(ИсходнаяСтрока);

КонецПроцедуры

Процедура ДобавитьСтроку_СДополнением_ВернетОбъединениеСтрок() Экспорт

ИсходнаяСтрока = ЮТест.Данные().СлучайнаяСтрока();
ДополнительнаяСтрока = ЮТест.Данные().СлучайнаяСтрока();

Результат = ЮТСтроки.ДобавитьСтроку(ИсходнаяСтрока, ДополнительнаяСтрока);

ЮТест.ОжидаетЧто(Результат)
.НачинаетсяС(ИсходнаяСтрока)
.ЗаканчиваетсяНа(ДополнительнаяСтрока)
;

КонецПроцедуры

Процедура ДобавитьСтроку_СУказаннымРазделителем_ВернетОбъединениеСтрокИУказанногоРазделителя() Экспорт

ИсходнаяСтрока = ЮТест.Данные().СлучайнаяСтрока();
ДополнительнаяСтрока = ЮТест.Данные().СлучайнаяСтрока();
Разделитель = ЮТест.Данные().СлучайнаяСтрока();
ОжидаемыйРезультат = СтрШаблон("%1%2%3", ИсходнаяСтрока, ДополнительнаяСтрока, Разделитель)

Результат = ЮТСтроки.ДобавитьСтроку(ИсходнаяСтрока, ДополнительнаяСтрока, Разделитель);

ЮТест.ОжидаетЧто(Результат)
.НачинаетсяС(ИсходнаяСтрока)
.Содержит(Разделитель)
.ЗаканчиваетсяНа(ДополнительнаяСтрока)
.Равно(ОжидаемыйРезультат)
;

КонецПроцедуры