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

Вы должны тестировать только через публичный контракт

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

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

Если вы напрямую тестируете какой-либо из непубличных методов, вы тесно свяжете свой тест с деталями реализации. Модульные тесты (или любые автоматизированные тесты) не должны проверять детали реализации, а должны проверять результаты в определенных сценариях.

«Тесты должны быть связаны с поведением кода и отделены от его структуры», — Кент Бек.

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

Но мне действительно нужно протестировать внутренний метод…

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

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

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

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

Не все зависимости нужно имитировать/заглушать

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

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

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

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

Рассмотрим этот простой пример кода:

public char GetChar(string str, int index)
{
   // get the character at index position from string
}

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

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

Например, что происходит:

  • если str равно нулю?
  • если str пусто ("")?
  • если index минус число?
  • если index больше длины str?

и т. д.

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

Покрытие кода действительно полезно только для одной вещи

Покрытие кода — это инструмент, который должны использовать только разработчики (которые пишут модульные тесты). У него есть одно основное применение: помочь разработчикам определить сценарии для тестов, которые в настоящее время не были покрыты.

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

Команды также не должны иметь привычку устанавливать произвольные цели охвата (например, «80% к июню!»). Это ничего не дает, кроме как побуждает разработчиков играть с системой и писать потенциально плохие или даже бесполезные модульные тесты. Единственная разумная цель для охвата на самом деле — 100%, но это редко число, на которое может рассчитывать любой разумный набор тестов.

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

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

Большинство юнит-тестов, написанных в наши дни, придерживаются шаблона «Устрой, действуй, утверждай» (AAA). Итак, давайте посмотрим, где мы можем улучшить размер теста в каждой из этих трех областей:

Упорядочить

  • Аранжировка (или настройка) часть теста обычно является основным виновником раздувания размера теста.
  • Код аранжировки должен быть извлечен другими методами, где это разумно возможно.
  • Затем извлеченные методы часто можно перемещать в новые классы, подальше от тестового класса, что делает сам тестовый класс меньше.
  • Попробуйте использовать простые шаблоны фабрики или построителя, чтобы создать необходимые сущности для ваших тестов.

Действовать

  • Действие — это точка выполнения фрагмента кода в определенном сценарии (определяемом аранжировкой), который затем ожидает определенный результат (утверждение). В вашем модульном тесте должен быть только один акт.
  • Если в вашем тесте более одного действия, то, скорее всего, вы выполняете более одного отдельного теста, но в рамках одного метода тестирования. Извлекайте код из других новых методов тестирования, пока в вашем тесте не останется только один Act.

Подтвердить

  • Теоретически, используя шаблон «Установить, действовать, утвердить», ваш тест должен иметь только одно утверждение.
  • Однако в некоторых сценариях может быть более практичным иметь несколько операторов Assert. Например, если вам нужно подтвердить ряд свойств в возвращаемом результате действия.
  • Опять же, как и в Arrange, если вы обнаружите, что пишете этот код более одного раза, извлеките набор операторов Assert в новый метод и вместо этого вызовите его.

Иметь стандарт имени теста

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

Например, простой стандарт для определения модульного теста может следовать такому шаблону, как:

[Test]
public void When<SCENARIO>_Then<EXPECTED_OUTCOME>()
{
   // test body
}

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

Некоторым разработчикам нравится использовать в своем тестовом шаблоне слова «Дано», «Должен» и т. д. С годами я остановился на «Когда» и «Тогда» просто потому, что они несут адекватный смысл и являются короткими (названия методов тестирования часто имеют тенденцию быть довольно длинными).

Также обратите внимание, что использование «Когда» и «Тогда» в этом случае не следует путать с их использованием в DSL «Огурец» («Дано/Когда/Тогда»).