Практические рекомендации
Причины, позволяющие программистам оправдать отсутствие тестов, сами по себе с введением TDD никуда не исчезают. Но TDD предоставляет простые средства, позволяющие почти полностью нейтрализовать их негативное влияние.
Перейдем к практическим рекомендациям. Несмотря на то что концепция модульного тестирования относительно проста, использование TDD в реальных проектах требует от программистов, и особенно от "техлидов", определенных навыков. Прежде всего, для успешного применения TDD необходимо умение собирать и интерпретировать некоторые стандартные тестировочные метрики. В XP-группе для оценки качества тестирования применяются:
- коэффициент покрытия кода (code coverage);
- количество тестов;
- количество asserts;
- количество строк кода в модульных тестах;
- суммарное время исполнения тестов.
Наиболее важный показатель - коэффициент тестового покрытия, или code coverage, измеряемый как отношение числа инструкций, выполненных тестами, к общему числу инструкций в модуле или приложении. Общий coverage приложения является основным средством оценки полноты модульного тестирования, и в нашем случае даже существует соглашение с заказчиком относительно его минимально допустимого уровня. Как правило, удовлетворительным считается coverage не ниже 75% или более, в зависимости от конкретного приложения. 100% сoverage не является чем-то из ряда вон выходящим и достаточно легко достигается при использовании "Test First". Использовать сoverage для оценки состояния модульных тестов следует осторожно. Эта метрика скорее позволяет выявить проблемы, чем указать на их отсутствие. "Плохие" значения coverage четко сигнализируют о том, что тестов в приложении недостаточно, в то время как "хорошие" значения не позволяют сделать обратного вывода. Проблема в том, что полнота тестов никак не связана с их корректностью. За рамками coverage остается также важный вопрос о диапазонах параметров функций. Тем не менее coverage удобно применять, с одной стороны, для общего наблюдения за тестированием в проекте, а с другой - для выявления не покрытых тестами участков кода.
Три других показателя обычно имеет смысл рассматривать вместе. В отличие от coverage, количество тестов, asserts и строк кода интереснее всего наблюдать в динамике. При нормальном использовании TDD все три значения должны расти ежедневно и равномерно, причины резких изменений необходимо выявлять.
Некоторый интерес представляет анализ отношений между этими и другими метриками: например, большая разница между количеством asserts и тестов может говорить о том, что тесты в среднем крупнее, чем нужно. Подтвердить или опровергнуть это утверждение может среднее количество строчек кода в тесте. Иногда имеет смысл рассматривать такие показатели, как среднее количество ежедневно добавляемых тестов, отношение тестов к основному коду по количеству строк и другие, но это скорее уже экзотика.
Время исполнения тестов выступает важной метрикой по двум причинам. Во-первых, резкое ухудшение времени исполнения тестов является достаточно надежным сигналом появления проблем с производительностью приложения. Во-вторых, чем дольше выполняются тесты, тем менее удобно с ними работать и тем реже программисты их запускают. Поэтому необходимо следить за тем, чтобы время работы тестов не увеличивалось по небрежности, а иногда имеет смысл даже прилагать некоторые усилия по их оптимизации.
В заключение разговора о метриках отмечу, что в XP-группе хорошо зарекомендовала себя практика их ежедневного автоматизированного сбора с рассылкой report'а команде. Анализом результатов, как правило, занимаются "PM" и "техлид". Метрики, несмотря на удобство работы с ними, в большинстве случаев не позволяют оценить тесты по целому ряду важных неформальных критериев. Поэтому существует набор требований к тестам, отслеживаемых, как правило, на code review. К ним относятся:
- Простота. Тесты кроме всего остального должны объяснять код, который они используют, и делать это максимально прозрачно для постороннего человека образом.
- Правильное именование тестов. Существуют разные соглашения, но в любом случае название должно адекватно отражать суть теста.
- Не допускается зависимость тестов друг от друга или от порядка вызова.
Выполнение этого правила позволяет отлаживать тесты произвольными группами. - Тесты должны быть атомарными, каждый из них должен проверять ровно один тестовый случай. Громоздкие и сложные тесты необходимо разбивать на несколько более мелких.
В соответствии с идеями TDD в большинстве случаев, кроме тестов, необходимых для обеспечения coverage, программисты пишут дополнительные тесты на ситуации, которые они по каким-либо причинам хотят проверить дополнительно. Такая практика приветствуется. Модульное тестирование, как уже отмечалось, не является "серебряной пулей" и может использоваться далеко не всегда. Так, например:
- Обычно в модульных тестах не проверяется performance. С технической точки зрения, это можно делать, и некоторые тестировочные среды даже предоставляют для этого специальные средства. Но требования по производительности обычно указываются для приложения в целом, а не для отдельных функций; кроме того, performance testing часто занимает большое количество времени.
- Как правило, не имеет смысла тестировать чужой код и автоматически сгенерированный код.
- На уровне модульного тестирования часто тяжело или невозможно проверять сложные функциональные требования к приложению. Этим у нас занимаются тестеры с помощью автоматизации или ручного тестирования.
Модульное тестирование - это специфическая область программирования. Чтобы получить общее представление о его особенностях, рассмотрим некоторые паттерны, применяющиеся в программировании тестов.
Необходимость запускать тесты отдельными наборами заставляет использовать механизмы структурирования тестов. В зависимости от конкретной среды, тесты организуют либо в иерархические наборы (Boost Test Library), либо в пространства имен (NUnit). Многие frameworks, кроме того, предоставляют возможность задать категорию теста, это удобно для того, чтобы запускать тесты из разных веток одновременно. Правила категоризации тестов имеет смысл определять в стандартах кодирования.
Важнейшим паттерном модульного тестирования, поддерживаемым всеми развитыми frameworks, являются SetUp/TearDown-методы, предоставляющие возможность выполнения кода перед и после запуска теста или набора тестов. Как правило, существуют отдельные методы SetUp/TearDown уровня тестов и test suites. Важной и очень удобной возможностью являются SetUp/TearDown-методы для всех тестов (уровень сборки в терминах mbUnit).
Основная задача SetUp/TearDown - как правило, создание тестовых наборов данных. При тестировании кода, работающего с базами данных на запись, в этих методах производится backup и восстановление базы либо создаются и откатываются транзакции.
Mock-объекты - тестировочный паттерн, суть которого состоит в замене объектов, используемых тестируемым кодом, на отладочные эквиваленты. Например, для тестирования кода, обрабатывающего обрыв коннекции к базе данных, вместо настоящей коннекции можно использовать специальный mock-объект, постоянно выбрасывающий нужное исключение.
Mock-объекты удобно использовать, если:
- заменяемый объект не обладает необходимым быстродействием;
- заменяемый объект тяжело настраивать;
- нужное поведение заменяемого объекта сложно смоделировать;
- для проверки call-back-функций;
- для тестирования GUI.
На практике регулярно приходится тестировать один и тот же код с разными комбинациями параметров. Row Test - это тестовая функция, принимающая несколько предопределенных наборов значений:
[RowTest] [Row("Monday", 1)] ... [Row("Saturday", 7)] public void TestGetDayOfWeekName(string result, int arg) { Assert.AreEqual(result, Converter.GetDayOfWeekName(arg)); } Combinatorial Test - тестовая функция, проверяющая код на всех возможных комбинациях для одного или нескольких массивов значений:
[Factory] public static int[] Numbers() { int[] result = { 1, ..., 9 }; return result; } [CombinatorialTest ] public void TestMultiplicationTable ( [UsingFactories("Numbers") int lhs, [UsingFactories("Numbers") int rhs) { Assert.AreEqual(lhs * rhs, Foo.Multiply(lhs, rhs)); } Прямая поддержка комбинаторного и строчного тестирования во framework серьезно облегчает тестирование чувствительного к параметрам кода. Хорошим примером такого framework является mbUnit.
Инструментарий модульного тестирования богат и разнообразен. Разница в ощущениях между использованием правильного и неправильного средства также будет большой. Не вдаваясь в описание конкретных продуктов, рассмотрим основные виды ПО, применяемого для модульного тестирования.
Главный инструмент модульного тестирования, конечно, unit test framework. Большинство современных framework базируются на дизайне, предложенном Беком в 1994 году в статье "Simple Smalltalk Testing". Задача framework - предоставлять библиотеки для создания тестов и средства их запуска. При выборе framework, с технической точки зрения, наиболее важно учитывать наличие необходимых клиентов (командная строка, GUI, модули для запуска из-под NAnt/Ant или IDE), поддержку используемых паттернов тестирования и reporting.
Поддержка тестирования из IDE в идеале должна включать в себя средства для запуска тестов по одному и группами с разной гранулярностью, под отладчиком и без него. Полезной является возможность измерения coverage для классов и сборок.Для применения "Test First" удобны средства генерации пустых определений для еще ненаписанных методов.