Какой процент функционала в приложении должен быть покрыт юнит тестами
Эта статья является конспектом книги «Принципы юнит-тестирования».
Давайте для начала перечислим свойства хороших юнит-тестов.
Первое. Интегрированы в цикл разработки. Пользу приносят только те тесты, которые вы активно используете; иначе писать их нет смысла.
Второе. Тестируют только самые важные части вашего кода. Не весь рабочий код заслуживает одинакового внимания.
Третье. Дают максимальную защиту от багов с минимальными затратами на сопровождение. Для этого нужно уметь распознавать эффективные тесты и писать их.
Однако распознавание и написание эффективного теста – два разных навыка. И для приобретения второго навыка необходимо сначала освоить первый. Далее в этой статье будет показано, как распознать эффективный тест. Также будет рассмотрена пирамида тестирования и тестирование по принципу «черного ящика» / «белого ящика».
Четыре аспекта хороших юнит-тестов
Хороший юнит-тест должен обладать следующими атрибутами: защита от багов, устойчивость к рефакторингу, быстрая обратная связь и простота поддержки.
Эти четыре атрибута фундаментальны. Они могут использоваться для анализа любых автоматизированных тестов, будь то юнит-, интеграционные или сквозные (end-to-end) тесты.
Начнем с первого атрибута хорошего юнит-теста: защиты от багов. Баг (или регрессия) — это программная ошибка. Как правило, такие ошибки возникают после внесения изменений в код.
Чем больше функциональности, тем выше вероятность того, что внесете баг в новую версию. Вот почему так важно разработать хорошую защиту от багов. Без такой защиты будет невозможно или очень сложно обеспечить рост проекта в долгосрочной перспективе из-за постоянно увеличивающегося количества ошибок.
Для оценки того, насколько хорошо тест проявляет себя в отношении защиты от багов, необходимо принять во внимание, следующее:
объем кода, выполняемого тестом;
сложность этого кода;
важность этого кода с точки зрения бизнес-логики.
Как правило, чем больше кода тест выполняет, тем выше вероятность выявить в нем баг. Само собой, тест также должен иметь актуальный набор проверок (assertions).
Важен не только объем кода, но и его сложность и важность с точки зрения бизнес-логики. Код, содержащий сложную бизнес-логику, важнее инфраструктурного кода — ошибки в критичной для бизнеса функциональности наносят наибольший ущерб.
Как следствие, тестирование тривиального кода обычно не имеет смысла. Этот код слишком простой и не содержит сколько-нибудь значительного объема бизнес-логики.
Второй атрибут хорошего юнит-теста — устойчивость к рефакторингу. Эта устойчивость определяет, насколько хорошо тест может пережить рефакторинг тестируемого им кода без выдачи ошибок.
Рефакторингом называется модификация существующего кода без изменения его наблюдаемого поведения. Обычно рефакторинг проводится для улучшения нефункциональных характеристик кода: читаемости и простоты. Примеры рефакторинга — переименование метода или выделение фрагмента кода в новый класс.
Рефакторинг может привести к ложному срабатыванию. Это ложный сигнал тревоги: тест показывает, что функциональность не работает, тогда как в действительности все работает как положено. Такие ложные срабатывания обычно происходят при рефакторинге кода, когда вы изменяете имплементацию, но оставляете поведение приложения без изменений. Чем меньше ложных срабатываний, тем лучше устойчивость к рефакторингу.
Почему столько внимания уделяется ложным срабатываниям? Потому что они могут иметь серьезные последствия для всего приложения. Целью юнит-тестирования является обеспечение устойчивого роста проекта. Устойчивый рост становится возможным благодаря тому, что тесты позволяют добавлять новую функциональность и проводить регулярный рефакторинг без внесения ошибок в код.
Частые ложные срабатывания могут привести к следующим ситуациям:
Если тесты падают без веской причины, они притупляют вашу готовность реагировать на проблемы в коде. Со временем вы привыкаете к таким сбоям и перестаете обращать на них внимание. А это может привести к игнорированию настоящих ошибок.
Начинаете все меньше и меньше доверять вашим тестам. Они уже не воспринимаются как что-то, на что вы можете положиться. Отсутствие доверия приводит к уменьшению рефакторинга, так как вы пытаетесь свести к минимуму потенциальные ошибки.
Что приводит к ложному срабатыванию?
Количество ложных срабатываний, выданных тестом, напрямую связано со структурой этого теста. Чем сильнее тест связан с деталями имплементации тестируемой системы, тем больше ложных срабатываний он порождает. Уменьшить количество ложных срабатываний можно только одним способом: отвязав тест от деталей имплементации тестируемой системы. Тест должен проверять конечный результат — наблюдаемое поведение тестируемой системы, а не действия, которые она совершает для достижения этого результата.
Лучший вариант структурирования теста — тот, при котором он рассказывает историю о предметной области. Если такой тест не проходит, это означает, что между историей и фактическим поведением приложения существует разрыв. Только такие падения тестов полезны — они всегда несут полезную информацию о том, что пошло не так.
Рис. 1 – Тест слева связан с наблюдаемым поведением SUT, а не с деталями реализации. Такой тест более устойчив к рефакторингу, чем тест справа
Связь между первыми двумя атрибутами
Между первыми двумя аспектами хорошего юнит-теста (защита от багов и устойчивость к рефакторингу) существует связь. Оба атрибута вносят вклад в точность тестов, хотя и с противоположных позиций. Эти два атрибута также по-разному влияют на проект с течением времени: важно иметь хорошую защиту от багов сразу же после запуска проекта, но необходимость в устойчивости к рефакторингу возникает позднее.
Давайте рассмотрим более широкую картину того, что собой представляют результаты тестовых прогонов. Тесты могут проходить или не проходить (строки таблицы), а сама функциональность может работать либо правильно, либо неправильно (столбцы таблицы).
Ситуация, когда тест проходит, а тестируемая функциональность работает правильно, называется истинным отрицательным срабатыванием: тест правильно определяет состояние системы (отсутствие в ней ошибок).
Рис. 2 - Отношение между защитой от багов и устойчивостью к рефакторингу
Если тест не выявляет ошибку, значит, возникла проблема. Ситуация соответствует правому верхнему квадранту: ложноотрицательное срабатывание. И именно ее помогает избежать защита от багов. Тесты с хорошей защитой от багов помогают минимизировать количество ложноотрицательных срабатываний — ошибок II типа.
С другой стороны, существует симметричная ситуация: функциональность работает правильно, но тест сообщает об ошибке. Это ложное срабатывание. И с ней помогает устойчивость к рефакторингу.
Количества ложных и ложноотрицательных срабатываний образуют метрику точности теста: чем меньше таких срабатываний, тем точнее тест. Сама метрика точности состоит из двух компонентов:
насколько хорошо тест выявляет присутствие ошибок (отсутствие ложноотрицательных срабатываний, сфера защиты от багов);
насколько хорошо тест выявляет отсутствие ошибок (отсутствие ложных срабатываний, сфера устойчивости к рефакторингу).
Как видно из формулы на рис. 3, улучшить точность теста можно двумя способами. Первый — повышение числителя (сигнал), то есть повышение вероятности выявления ошибок. Второй — уменьшение знаменателя (шум), то есть уменьшения вероятности ложных срабатываний.
Рис. 3 – Формула точности теста
Третий и четвертый аспекты: быстрая обратная связь и простота поддержки
Быстрая обратная связь является одним из важнейших свойств юнит-теста. Чем быстрее работают тесты, тем больше их можно включить в проект и тем чаще вы их сможете запускать. В результате чего затраты на исправление этих ошибок уменьшаются почти до нуля. С другой стороны, медленные тесты увеличивают время, в течение которого ошибки остаются необнаруженными, что приводит к увеличению затрат на их исправление.
Простота поддержки оценивает затраты на сопровождение кода. Метрика состоит из двух компонентов:
Насколько сложно тест понять. Этот компонент связан с размером теста. Чем меньше кода в тесте, тем проще он читается и проще изменяется при необходимости.
Насколько сложно тест запустить. Если тест работает с внепроцессными зависимостями, вам придется тратить время на то, чтобы поддерживать эти зависимости в рабочем состоянии.
В поисках идеального теста
Произведение этих четырех атрибутов определяет эффективность теста. И в данном случае автор книги использует термин «произведение» в математическом смысле: если один из атрибутов равен нулю, то ценность всего теста тоже обращается в нуль.
Установите достаточно высокий порог для минимальной требуемой эффективности и включайте в проект только те тесты, которые проходят этот порог. Небольшой набор высокоэффективных тестов намного лучше справится с задачей поддержания роста проекта, чем большое количество посредственных тестов.
Возможно ли создать идеальный тест? К сожалению, получить максимальные оценки по всем четырем показателям невозможно. Дело в том, что первые три атрибута — защита от багов, устойчивость к рефакторингу и быстрая обратная связь — являются взаимоисключающими. Невозможно довести их до максимума одновременно: одним из трех придется пожертвовать для максимизации двух остальных.
Нельзя просто обнулить один атрибут, чтобы сосредоточиться на остальных. Как упоминалось ранее, тест с нулевым значением в одной из четырех категорий бесполезен. Следовательно, атрибуты нужно максимизировать так, чтобы ни один из них не падал слишком низко.
Первый пример — сквозные (end-to-end) тесты. Сквозные тесты рассматривают систему с точки зрения конечного пользователя. Они обычно проходят через все компоненты системы, включая пользовательский интерфейс, базу данных и внешние приложения.
Так как сквозные тесты задействуют большой объем кода, они обеспечивают наилучшую защиту от багов. Сквозные тесты практически не выдают ложных срабатываний, а, следовательно, обладают хорошей устойчивостью к рефакторингу. Они не настаивают на какой-то конкретной имплементации, смотрят только на поведение приложения с точки зрения конечного пользователя.
Однако у сквозных тестов имеется крупный недостаток: они очень медленные. Любой проект, который полагается исключительно на такие тесты, не сможет получить быструю обратную связь. Именно поэтому невозможно обеспечить покрытие кода только сквозными тестами.
Второй пример максимизации двух из трех атрибутов за счет третьего — тривиальный тест. Такие тесты покрывают простой фрагмент кода, вероятность сбоя в котором невелика.
Рис. 4 - Тривиальный тест, покрывающий простой фрагмент кода
В отличие от сквозных тестов, тривиальные тесты предоставляют быструю обратную связь. Кроме того, вероятность ложных срабатываний также мала, поэтому они обладают хорошей устойчивостью к рефакторингу. Тем не менее тривиальные тесты вряд ли смогут выявить какие-либо ошибки, потому что покрываемый ими код слишком прост.
Третий пример - тест, который работает быстро и хорошо выявляет ошибки в коде, но делает это с большим количеством ложных срабатываний. Такие тесты называются хрупкими: они падают при любом рефакторинге тестируемого кода независимо от того, изменилась тестируемая ими функциональность или нет.
Рис. 5 – Места, которые занимают тесты по отношению друг к другу
Четвертый атрибут — простота поддержки — не так сильно связан с первыми тремя, за исключением сквозных (end-to-end) тестов. Сквозные тесты обычно имеют больший размер из-за необходимости подготовки всех зависимостей, к которыми могут обращаться такие тесты. Они также требуют дополнительных усилий для поддержания этих зависимостей в работоспособном состоянии. Таким образом, сквозные тесты требуют больших затрат на сопровождение.
Выдержать баланс между атрибутами хорошего теста сложно. Тест не может иметь максимальных значений в каждой из первых трех категорий; также приходится учитывать аспект простоты поддержки. А значит, вам придется идти на компромиссы. Более того, на компромиссы придется идти так, чтобы ни один конкретный атрибут не оказался равным нулю. Уступки должны быть частичными и стратегическими.
По мнению автора книги, лучшие тесты демонстрируют максимально возможную простоту поддержки и устойчивость к рефакторингу; всегда старайтесь максимизировать эти два атрибута. Компромисс сводится к выбору между защитой от багов и быстротой обратной связи.
Рис. 6 – Компромиссы между атрибутами хорошего теста
Почему же устойчивость к рефакторингу не должна быть предметом для компромиссов? Потому что этот атрибут в основном сводится к бинарному выбору: тест либо устойчив к рефакторингу, либо нет. Между этими двумя состояниями почти нет промежуточных ступеней. А значит, пожертвовать небольшой частью устойчивости к рефакторингу не получится. С другой стороны, метрики защиты от багов и быстрой обратной связи более эластичны.
Компромисс между первыми тремя атрибутами хорошего юнит-теста напоминает теорему CAP. Эта теорема утверждает, что распределенное хранилище данных не может предоставить более двух из трех гарантий одновременно: согласованность (consistency) данных, доступность (availability), устойчивость к разделению (partition tolerance).
Сходство является двойным:
1. В CAP вы тоже можете выбрать максимум два атрибута из трех;
2. Устойчивость к разделению в крупномасштабных распределенных системах также не является предметом для компромиссов. Большое приложение — такое как, например, веб-сайт Amazon — не может работать на одной машине. Вариант с достижением согласованности данных и доступности за счет устойчивости к разделению просто не рассматривается.
Пирамида тестирования
Концепция пирамиды тестирования предписывает определенное соотношение разных типов тестов в проекте: юнит-тесты, интеграционные тесты, сквозные тесты.
Пирамида тестирования часто изображается состоящей из трех типов тестов. Ширина уровней пирамиды обозначает относительную долю тестов определенного типа в проекте. Чем шире уровень, тем больше тестов. Высота уровня показывает, насколько близки эти тесты к эмуляции поведения конечного пользователя. Разные типы тестов в пирамиде выбирают разные компромиссы между быстротой обратной связи и защитой от багов. Тесты более высоких уровней пирамиды отдают предпочтение защите от багов, тогда как тесты нижних уровней выводят на первый план скорость выполнения.
Рис. 7 - Пирамида тестирования предписывает определенное соотношение юнит-, интеграционных и сквозных тестов Рис. 8 - Разные типы тестов в пирамиде принимают разные решения относительно быстрой обратной связи и защиты от багов
Точное соотношение между типами тестов будет разным для разных команд и проектов. Но в общем случае должно сохраняться соотношение пирамиды: сквозные тесты составляют меньшинство; юнит-тесты — большинство; интеграционные тесты лежат где-то в середине.
Причина, по которой сквозных тестов меньше всего – исключительно низкая скорость выполнения. Они также не отличаются простотой в поддержке: такие тесты обычно занимают много места и требуют дополнительных усилий для поддержания задействованных внепроцессных зависимостей. Таким образом, сквозные тесты имеет смысл применять только к самой критической функциональности.
У пирамиды тестирования есть исключения. Юнит-тесты менее полезны в ситуациях, в которых отсутствует алгоритмическая или бизнес-сложность, — они быстро вырождаются в тривиальные тесты. В то же время интеграционные тесты полезны даже в таких случаях; каким бы простым код ни был, важно проверить, как он работает в интеграции с другими подсистемами (например, базой данных). В результате в CRUD-приложениях у вас будет меньше юнит-тестов и больше интеграционных.
Другое исключение из пирамиды тестирования — API, обращающиеся к единственной внепроцессной зависимости (например, базе данных). В таких приложениях логично задействовать больше сквозных тестов. Так как пользовательский интерфейс отсутствует, сквозные тесты будут выполняться достаточно быстро. Затраты на сопровождение тоже будут не особенно велики, потому что вы работаете только с одной внешней зависимостью — базой данных.
Выбор между тестированием по принципу «черного ящика» и «белого ящика»
Тестирование по принципу «черного ящика» проверяет функциональность системы без знания ее внутренней структуры. Такое тестирование обычно строится на основе спецификаций и требований. Оно проверяет, что должно делать приложение, а не то, как оно это делает.
Тестирование по принципу «белого ящика» работает по противоположному принципу. Этот метод тестирования проверяет внутренние механизмы приложения. Тесты строятся на основе исходного кода, а не на основе требований или спецификаций.
Рис. 9 - Достоинства и недостатки тестирования по принципу «черного ящика» и «белого ящика»
Как говорилось ранее, нельзя делать уступки в отношении устойчивости тестов к рефакторингу: тест либо хрупок, либо нет. Всегда отдавайте предпочтение тестированию по принципу «черного ящика». Тесты — неважно, юнит-, интеграционные или сквозные — должны рассматривать систему как «черный ящик» и проверять поведение, имеющее смысл с точки зрения бизнес-логики. Если тест не удается связать с бизнес-требованием, это является признаком хрупкости теста.
Системное тестирование (system testing) — тест высокого уровня для проверки работы большего куска приложения или системы в целом.
Регрессионное тестирование (regression testing) — тестирование, которое используется для проверки того, не влияют ли новые фичи или исправленные баги на существующий функционал приложения и не появляются ли старые баги.
Функциональное тестирование (functional testing) — проверка соответствия части приложения требованиям, заявленным в спецификациях, юзерсторях и т. д.
Виды функционального тестирования:
- тест «белого ящика» (white box) на соответствие части приложения требованиям со знанием внутренней реализации системы;
- тест «черного ящика» (black box) на соответствие части приложения требованиям без знания внутренней реализации системы.
Unit — модульные тесты, применяемые в различных слоях приложения, тестирующие наименьшую делимую логику приложения: например, класс, но чаще всего — метод. Эти тесты обычно стараются по максимуму изолировать от внешней логики, то есть создать иллюзию того, что остальная часть приложения работает в стандартном режиме.
Данных тестов всегда должно быть много (больше, чем остальных видов), так как они тестируют маленькие кусочки и весьма легковесные, не кушающие много ресурсов (под ресурсами я имею виду оперативную память и время).
Integration — интеграционное тестирование. Оно проверяет более крупные кусочки системы, то есть это либо объединение нескольких кусочков логики (несколько методов или классов), либо корректность работы с внешним компонентом. Этих тестов как правило меньше, чем Unit, так как они тяжеловеснее.
UI — тесты, которые проверяют работу пользовательского интерфейса. Они затрагивают логику на всех уровнях приложения, из-за чего их еще называют сквозными. Их как правило в разы меньше, так они наиболее тяжеловесны и должны проверять самые необходимые (используемые) пути.
На рисунке выше мы видим соотношение площадей разных частей треугольника: примерно такая же пропорция сохраняется в количестве этих тестов в реальной работе.
Сегодня подробно рассмотрим самые используемые тесты — юнит-тесты, так как уметь ими пользоваться на базовом уровне должны все уважающие себя Java-разработчики.
Ключевые понятия юнит-тестирования
- материал о Code Coverage на JavaRush и на Хабре; .
- Пишем наш тест.
- Запускаем тест, прошел он или нет (видим, что всё красное — не психуем: так и должно быть).
- Добавляем код, который должен удовлетворить данный тест (запускаем тест).
- Выполняем рефакторинг кода.
Этапы тестирования
- Задание тестируемых данных (фикстур).
- Использование тестируемого кода (вызов тестируемого метода).
- Проверка результатов и сверка с ожидаемыми.
Среды тестирования
- assertEquals(Object expecteds, Object actuals) — проверяет, равны ли передаваемые обьекты.
- assertTrue(boolean flag) — проверяет, возвращает ли переданное значение — true.
- assertFalse(boolean flag) — проверяет, возвращает ли переданное значение — false.
- assertNull(Object object) – проверяет, является ли объект нулевым (null).
- assertSame(Object firstObject, Object secondObject) — проверяет, ссылаются ли передаваемые значения на один и тот же обьект.
- assertThat(T t, Matcher<T> matcher) — проверяет, удовлетворяет ли t условию, указанному в matcher.
Практика тестирования
А теперь давайте рассмотрим приведенный выше материал на конкретном примере. Будем тестировать метод для сервиса — update. Рассматривать слой дао не будем, так как он у нас дефолтный. Добавим стартер для тестов: Итак, класс сервиса: 8 — вытягиваем обновляемый обьект из БД 9-14 — создаём объект через билдер, если в приходящем объекте есть поле — задаем его, если нет — оставляем то, что есть в БД И смотрим наш тест: 1 — наш Runner 4 — изолируем сервис от слоя дао, подставляя мок 11 — задаем для класса тестовую сущность (ту, которую мы будем юзать в качестве испытуемого хомячка) 22 — задаём объект сервиса, который мы и будем тестить Здесь мы видим четкое разделение теста на три части: 3-9 — задание фикстур 11 — выполнение тестируемой части 13-17 — проверка результатов Подробнее: 3-4 — задаём поведение для мока дао 5 — задаём экземпляр, который мы будем апдейтить поверх нашего стандартного 11 — используем метод и берём результирующий экземпляр 13 — проверяем, что он не ноль 14 — сверяем айди результата и заданные аргументы метода 15 — проверяем, обновилось ли имя 16 — смотрим результат по cpu 17 – так как в экземпляре для обновления мы не задавали это поле, оно должно остаться прежним, проверяем это. Запускаем: Тест зелёный, можно выдыхать)) Итак, подведём итоги: тестирование улучшает качество кода и делает процесс разработки более гибким и надёжный. Представьте себе, как много сил мы потратим при изменении дизайна программного обеспечения с сотнями файлов классов. Когда у нас есть модульные тесты, написанные для всех этих классов, мы можем уверенно провести рефакторинг. И самое главное — это помогает нам легко находить ошибки во время разработки. Гайз, на этом у меня сегодня всё: сыпем лайки, пишем комменты)))В очередной раз Joel Spolsky, автор отличных книг из серии Joel on Software и одноименного блога JoelOnSoftware, написал потрясающую статью. В этот раз он рассуждает про Test Driven development и, как обычно, делает это без всякого уважения к авторитетам и современным тенденциям.
Должен признаться, что моё отношение к TDD в точности совпадает с тем, что он описал в этой статье. И это радует и успокаивает.
Попробую в этой статье раскрыть мое отношение к юнит тестам и TDD.
Для начала немного теории
100% покрытие кода юнит тестами
Итак, поводом для статьи Джоеля стали письма от людей, которые призывали его добавить 13-ый пункт в его знаменитые 12 шагов, чтобы писать код лучше. Его призывали добавить пункт Юнит тесты, 100% вашего кода покрыто юнит тестами.
Консультанты твердят то же самое. И только разработчики чувствуют себя ущербными, так как НЕ МОГУТ сделать 100% покрытие кода тестами. Они читают книги про TDD, слушают консультантов, тратят время, но 100% покрытия достичь все равно не могут. Вы, например, можете?
Почему же так происходит и что делать?
Никто из разработчиков не хочет тратить месяцы и годы для покрытия тестами кода, который работает стабильно, уже 5-10 лет не менялся и еще 10 лет не будет меняться. Но руководство нанимает консультантов и те убеждают, то без 100% покрытия тестами невозможно повысить качество продукта. Ставится такая задача, все работают в поте лица, а качество программы падает каждый день.
И это нам позволяет сделать главный вывод: 100% покрытие юнит тестами вредно. Даже так: сама идея о 100% покрытии тестами вредна! Покрывать надо только те куски кода, которые будут меняться, причём несерьезно, так как при серьезных изменениях придется переделывать и все тесты, а значит старые станут бесполезной обузой.
А много ли кода, подпадающего под это определение обычно в программе? Это и есть те самые 5-10%. Так что обычно достаточно покрыть тестами 5-10% кода для получения всех преимуществ юнит тестов. Если юнит тестов становится больше, то они перестают приносить пользу, но увеличивают расходы на поддержание их работоспособности.
Например, Michael Feathers в своей книге Working Effectively with Legacy Codeне призывает покрывать все 100% кода тестами.
Моё отношение к юнит тестам и TDD
Но я противник стопроцентного покрытия кода тестами и применения TDD для работы с legacy кодом.
Для каждой методологии есть свое применение. И нет никакой серебряной пули, помогающей всегда.
А вы когда-нибудь задумывались о необходимости тестирования разрабатываемых приложений? Сегодня я попробую показать важность применения unit-тестов, которые призваны помочь в обнаружении ошибок на ранних этапах работы, что в последующем приводит к экономии ваших средств и ресурсов.
В процессе написания ПО у меня возникло понимание о целесообразности применения unit-тестов.
В моей практике появилось несколько проектов, в которых мне довелось писать unit-тесты, каждый из которых выполнял определенную роль — поиск ошибок в основных алгоритмах кода, нагрузочное тестирование и отладка бэкенда веб-приложения.
В каждой из поставленных задач unit-тесты оказались эффективны, позволив существенно сократить время работы и обеспечить своевременное обнаружение ошибок кода.
Согласно данным[1] исследований, цена ошибки в ходе разработки и поддержании ПО экспоненциально возрастает при несвоевременном их обнаружении.
На представленном рисунке видно, что при выявлении ошибки на этапе формирования требований мы получим экономию средств в соотношении 200:1 по сравнению с их обнаружением на этапе поддержки.
Среди всех тестов львиную долю занимают именно unit-тесты. В классическом понимании unit-тесты позволяют быстро и автоматически протестировать отдельные части ПО независимо от остальных.
Рассмотрим простой пример создания unit-тестов. Для этого создадим консольное приложение Calc, которое умеет делить и суммировать числа.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Calc < class Program < static void Main(string[] args) < >> >Добавляем класс, в котором будут производиться математические операции.
using System; namespace Calc < /// <summary> /// Выполнение простых математических действий над числами /// </summary> public class Calculator < /// <summary> /// Получаем результат операции деления (n1 / n2) /// </summary> /// <param name="n1">Первое число</param> /// <param name="n2">Второе число</param> /// <returns>Результат</returns> public double Div(double n1, double n2) < // Проверка деления на "0" if (n2 == 0.0D) throw new DivideByZeroException(); return n1 / n2; >/// <summary> /// Получаем результат сложения чисел и их увеличения на единицу /// </summary> /// <param name="n1"></param> /// <param name="n2"></param> /// <returns></returns> public double AddWithInc(double n1, double n2) < return n1 + n2 + 1; >> >Так, в методе Div производится операция деления числа n1 на число n2. Если передаваемое число n2 будет равняться нулю, то такая ситуация приведет к исключению. Для этого знаменатель этой операции проверяется на равенство нулю.
Метод AddWithInc производит сложение двух передаваемых чисел и инкрементацию полученного результата суммирования на единицу.
На следующем шаге добавим в решение проект тестов.
Пустой проект unit-тестов:
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CalcTests < [TestClass] public class UnitTest1 < [TestMethod] public void TestMethod1() < >> >Переименуем наш проект: «SimpleCalculatorTests». Добавляем ссылку на проект Calc.
В проекте Calc содержатся 2 метода, которые надо протестировать на корректность работы. Для этого создадим 3 теста, которые будут проверять операцию деления двух чисел, операцию деления на нуль и операцию сложения двух чисел и инкрементацию полученной суммы.
Добавляем в проект тест для проверки метода AddWithInc.
/// <summary> /// Тест проверки метода AddWithInc /// </summary> [TestMethod] public void AddWithInc_2Plus3Inc1_Returned6() < // arrange var calc = new Calculator(); double arg1 = 2; double arg2 = 3; double expected = 6; // act double result = calc.AddWithInc(arg1, arg2); // assert Assert.AreEqual(expected, result); >В тесте создаются 3 переменные — это аргументы, передаваемые в метод AddWithInc, и ожидаемый результат, возвращаемый этим методом. Результат выполнения метода будет записан в переменную result.
На следующем шаге происходит сравнение ожидаемого результата с реальным числом метода AddWithInc. При совпадении результата с ожидаемым числом, то есть числом 6, тест будет считаться положительным и пройденным. Если полученный результат будет отличаться от числа 6, то тест считается проваленным.
Следующим тестом мы будем проверять метод Div
Аналогичным образом создаются два аргумента и ожидаемый результат выполнения метода Div. Если результат деления 4/2 в методе равен 2, то тест считается пройдённым. В противном случае — не пройденным.
Следующий тест будет проверять операцию деления на нуль в методе Div.
[TestMethod] [ExpectedException(typeof(DivideByZeroException), "Oh my god, we can't divison on zero")] public void Div_4Div0_ZeroDivException() < // arrange var calc = new Calculator(); double arg1 = 4; double arg2 = 0; // act double result = calc.Div(arg1, arg2); // assert >Тест будет считаться пройденным в случае возникновения исключения DivideByZeroException — деление на нуль. В отличии от двух предыдущих тестов, в этом тесте нет оператора Assert. Здесь обработка ожидаемого результата производится с помощью атрибута «ExpectedException».
Если аргумент 2 равен нулю, то в методе Divвозникнет исключение — деление на нуль. В таком случае тест считается пройденным. В случае, когда аргумент 2 будет отличен от нуля, тест считается проваленным.
Для запуска теста необходимо открыть окно Test Explorer. Для этого нажмите Test -> Windows -> Test Explorer (Ctrl+, T). В появившемся окне можно увидеть 3 добавленных теста:
Для запуска всех тестов нажмите Test -> Run -> All tests (Ctrl+, A).
Если тесты выполнятся успешно, в окне Test Explorer отобразятся зеленые пиктограммы, обозначающие успешность выполнения.
В противном случае пиктограммы будут красными.
Unit-тесты имеют обширную, строго не регламентированную область применения — зачастую фантазия самого автора кода подсказывает решение нестандартных задач с помощью этого инструмента.
Случай написания тестов для бэкенда веб-приложения в моей практике является не совсем стандартным вариантом применения unit-тестов. В данной ситуации unit-тесты вызывали методы контроллера MVC-приложения, в то же время передавая тестовые данные в контроллеры.
Далее в режиме отладки шаг за шагом выполнялись все действия алгоритма. В этом случае применение тестов позволило произвести быструю отладку бэкенда веб-приложения.
Существуют случаи, когда модульные тесты применять нецелесообразно. Например, если вы веб-разработчик, который делает сайты, где мало логики. В таких случаях имеются только представления, как, например, для сайтов-визиток, рекламных сайтов, или, когда вам поставлена задача реализовать пилотный проект «на посмотреть, что получится». У вас ограниченные ресурсы и время. А ПО будет работать только один день — для показа руководству.
Сжатые сроки, малый бюджет, размытые цели или довольно несложные требования — случаи, в которых вы не получите пользы от написания тестов.
Для определения целесообразности использования unit-тестов можно воспользоваться следующим методом: возьмите лист бумаги и ручку и проведите оси X и Y. X — алгоритмическая сложность, а Y — количество зависимостей. Ваш код поделим на 4 группы.
- Простой код (без каких-либо зависимостей)
- Сложный код (содержащий много зависимостей)
- Сложный код (без каких-либо зависимостей)
- Не очень сложный код (но с зависимостями)
Первое — это случай, когда все просто и тестировать здесь ничего не нужно.
Второе — случай, когда код состоит только из плотно переплетенных в один клубок реализаций, перекрестно вызывающих друг друга. Тут неплохо было бы провести рефакторинг. Именно поэтому тесты писать в этом случае не стоит, так как код все равно будет переписан.
Третье — случай алгоритмов, бизнес-логики и т.п. Важный код, поэтому его нужно покрыть тестами.
Четвертый случай — код объединяет различные компоненты системы. Не менее важный случай.
Последние два случая — это ответственная логика. Особенно важно писать тесты для ПО, которые влияют на жизни людей, экономическую безопасность, государственную безопасность и т.п.
Подводя итог всего описанного выше хочется отметить, что тестирование делает код стабильным и предсказуемым. Поэтому код, покрытый тестами, гораздо проще масштабировать и поддерживать, т.к. появляется большая доля уверенности, что в случае добавления нового функционала нигде ничего не сломается. И что не менее важно — такой код легче рефакторить.
[1] Данные взяты из книги «Технология разработки программного обеспечения» автора Ларисы Геннадьевны Гагариной
Читайте также: