Модульные тесты (или unit-тесты) - это программы, созданные для запуска других программ (называемых тестируемым кодом (Code Under Test, CUT) или продуктивным кодом (Production Code)) с конкретными предварительными условиями и проверкой ожидаемого поведения CUT.
Модульные тесты обычно написаны на том же языке программирования что и код, проверяемый ими.
Каждый unit-тест должен быть небольшим и проверять ограниченный объем функциональности. Тест-кейсы (Test cases) часто объединяются в Группы Тестов(Test Groups) или Наборы Тестов(Test Suites). Существует огромное количество фреймворков модульного тестирования. Самые популярные из них следуют шаблону xUnit, представленному Кентом Беком. Например, JUnit для Java или CppUTest для C/C++.
Модульные тесты также должны быть быстрыми. Обычно предполагается, что сотни тест-кейсов выполняются за несколько секунд.
Предназначение модульных тестов не в поиске ошибок. Они являются спецификацией ожидаемого поведения тестируемого кода. Этот тестируемый код как раз реализует ожидаемое поведение. Поэтому unit-тесты и тестируемый код используются для проверки корректности и защиты друг друга. Если кто-то модифицирует продуктивный код, и это изменяет первоначально заложенное поведение программы, то тест будет провален. Если ваш код покрыт адекватными unit-тестами, то вы можете его развивать, не боясь сломать текущий функционал. Вот почему Майкл Физерс описывает унаследованный код (legacy code), как код, непокрытый модульными тестами.
Предназначение модульного тестирования состоит в том, чтобы защитить уже реализованный функционал, нежели чем найти в нем дефекты. Это похоже на точки страховки, которые используют скалолазы в процессе подъёма на скалу. Такая страховка защищает от того, чтобы не упасть ниже уже набранной высоты.
Цель модульного теста в том, чтобы:
“Да, важно использовать автоматические тесты для защиты текущей функциональности. Но почему это необходимо на уровне модулей?”
Вы можете спросить, почему бы нам просто не использовать тщательные автоматизированные функциональные или системные тесты для такой цели?
Совокупная стоимость владения (Total cost of ownership, TCO) – Модульный тест находится на том же уровне абстракции системы, что и основной код. Это просто какой-то код, использующий другой код. Он не должен работать в той же среде, что и продуктивный код. Для компилируемых языков программирования даже не нужно использовать тот же компилятор, что и для продуктивной среды. Стоимость создания и запуска unit-теста очень низкая. При правильной разработке стоимость поддержки таких тестов также очень низкая. Вы можете не получить такой же уровень уверенности от одного успешного модульного тест-кейса, как вы получаете от функционального теста. Вам понадобится много небольших модульных тест-кейсов, чтобы получить примерно сравнимый уровень доверия. Но стоимость владения небольшими модульными тестами всё равно намного ниже, чем владение несколькими функциональными тестами.
Если кодовая база продукта не содержала каких-либо модульных тестов в течение последних 2 лет, то потребуется дополнительные расходы на внедрение модульного тестирования в таком коде. Стоимость в основном складывается из двух составляющих:
Внутреннее vs. Внешнее качество - Высокоуровневые автоматические тесты, такие как функциональные и системный тесты, проверяют внешнее качество программного обеспечения. Внешнее качество показывает, насколько хорошо программное обеспечение работает в соответствии с требованиями. Модульный тест не так эффективен, как функциональный тест для защиты внешнего качества. С другой стороны, модульное тестирование обеспечивает внутреннее качество программного обеспечения. Внутреннее качество здесь означает тестируемость кода и то, насколько хорошо он защищён. Дизайн с возможностью тестирования - это, в общем, хороший дизайн. Другие уровни автоматического тестирования не могут служить этой цели также хорошо, как модульное тестирование.
Качество обратной связи - Когда вы прошли функциональный тест, вы можете быть уверены в функциональности, которую вы только что протестировали. Но когда вы обнаружили, что тест выполнился неуспешно, обычно вам нужно выполнить отладку, чтобы увидеть, что не так. Модульный тест может дать вам более точную информацию о том, что работает, а что нет.
Модульный тест находится на уровне абстракции исходного кода языка программирования. При запуске модульного теста всё должно происходить внутри ЦПУ и памяти. Каждый тест-кейс должен быть небольшим и выполняться очень быстро. Обычно, вы должны иметь возможность запускать сотни модульных тестов за несколько секунд. Включая компиляцию и время других подготовок, весь процесс запуска тестов должен занимать не более 1 минуты.
Модульные тесты также должны быть повторяемы. Если ничего не поменялось в коде, результат работы модульного теста всегда один и тот же.
Если модульный тест очень быстрый и повторяемый, разработчики могут запускать их так часто, как хотят, хоть каждые несколько минут. Таким образом модульный тест будет непрерывно обеспечивать качественную обратную связь программисту. Следовательно, программист может двигаться вперёд с постоянным прогрессом и сосредоточиться на более важных вещах, а не тратить слишком много энергии на тривиальные вопросы.
Разумная структура автоматических тестов должна выглядеть как пирамида. На нижнем уровне большое количество модульных тестов. В середине чуть меньше интеграционных тест-кейсов. На вершине ещё меньше функциональных/системных тестов.
Это правда, что, в конце концов, продуктивный код составляет конечный продукт. Но большинство программных продуктов имеют эволюционный жизненный цикл. Код не является статичным. Он меняется со временем. Код без unit-теста не имеет необходимой защиты при изменении. Модульное тестирование также содержит важную информацию, которая не включена в продуктовый код.
Поэтому модульные тесты так же важны, как и тестируемый код. Они должны быть в одном и том же SCM-репозитории. Они должны следовать тем же стандартам кодирования, что и продуктивный код.
Предназначение модульных тестов не в нахождение ошибок. Технически, они скорее проверяют, а не тестируют, что тестируемый код ведёт себя так, как это предполагал разработчик, который его проектировал. Поэтому будет разумным решением дать возможность одному программисту написать тест и код, который он проверяет.
Также рекомендуется объединять двух или более человек для совместного программирования. Они пишут модульные тесты и тестируемый код вместе. Существует много занимательных видов парного программирования. Больше информации об этом вы можете найти в разделе “Разработка через тестирование” (Test-Driven Development).
Часто это не так. Если код не обладает хорошей тестируемостью, технически вы все равно можете написать для него модульный тест. Но это тест, написанный для такого кода, часто бывает довольно сложным в понимании и сопровождении. Поэтому, в нем нет особого смысла.
Секрет модульного теста не в написании самого теста, а в написании кода, пригодного для тестирования. Мы хотим иметь в итоге пригодный для тестирования код и простой тест, что выигрышно с обеих сторон. Мы не хотим иметь сложный в сопровождении код без возможности его тестирования, что было бы проигрышем и в одном и в другом.
Что ж, попробуйте попросить скалолаза зацепить страховочный крюк позже.
Если тест завершается успешно, он должен просто напечатать “OK” (и, возможно, несколько точек, чтобы показать прогресс). И никакой другой информации.
Практическое правило:
Вмешательства человека не должно требоваться для подготовки к тестам, запуска тест-кейсов или проверки их результатов.
И когда он падает, он должен предоставить точную информацию об этом. Цель состоит в сокращении времени, которое вы тратите на отладке упавших тестов.
“AAA” (Arrange, Act and Assert) – хороший шаблон для написания модульных тестов. (входные данные, действие, ожидаемый результат)
Если вы можете проследить использование такого подхода в каждом вашем тест-кейсе, то ваши тесты должны быть простыми для понимания, достаточно конкретными и по существу. Один модульный тест должен тестировать что-то одно. Следовательно, каждый тест-кейс должен содержать только один AAA-набор. Тест-кейс не должен быть слишком большим (больше 10 строк кода), если он следует шаблону ААА.
import unittest
class TestGroupForTextWrapping(unittest.TestCase):
def test_should_have_no_wrapping_when_string_length_is_5_and_line_width_is_10(self):
# Arrange: Arrange all necessary preconditions and inputs.
wrapper = TextWrapper(width=10)
# Act: Act on the object or method under test.
wrapped = wrapper.wrap("a" * 5)
# Assert: Assert that the expected results have occurred.
self.assertEqual(["a" * 5], wrapped)
Будучи похожим на AAA паттерн, подход BDD использует три других ключевых слова для описания каждого тест-кейса: Given, When and Then. (Вы также можете использовать And в качестве ключевого слова.) На русском: Дано, Когда, Тогда, И соответственно.
Given The Text Wrapper's Width Defined As 10
And Using '-' As Word Connector
When The Wrapper Wrap Text Length is Less Than 10
Then The Text Should Not Be Wrapped
Как вы видите, подход “given-when-then” почти аналогичен подходу “arrange-act-assert”. Они оба просто определяют переход из одного состояния в другое в Конечном Автомате (Finite State Machine, FSM). Более подробную информацию вы можете найти в статье “Дяди Боба” (Uncle Bob). Но между этими подходами есть ряд отличий:
В общем, хорошее правило для каждого модульного тест-кейса звучит так:
Каждый модульный тест-кейс должен быть сильно ограничен с точки зрения масштабов проверяемого участка системы
Поэтому:
Нет секрета в том, как написать хороший модульный тест. Для того чтобы это сделать, вы должны создавать “легко тестируемый” дизайн ваших систем.