Три цвета
С практической точки зрения, основой TDD является цикл "red/green/refactor". В первой фазе программист пишет тест, во второй - код, необходимый для того, чтобы тест работал, в третьей, при необходимости, производится рефакторинг. Последовательность фаз очень важна. В соответствии с принципом "Test First", следует писать только такой код, который абсолютно необходим, чтобы тесты выполнялись успешно.
Попробуем проиллюстрировать этот цикл простейшим примером. Допустим, нам необходим метод, преобразующий числа от 1 до 7 в названия соответствующих дней недели. Следуя принципу "Test First", вначале пишем тест для этого метода:
[Test, ExpectedException(typeof(ArgumentException))] public void TestGetDayOfWeekNameInvalidArgument() { Converter.GetDayOfWeekName(8); }Затем, создаем метод-заглушку на тестируемом классе, необходимую для того, чтобы проект собрался:
static public string GetDayOfWeekName(int dayNumber) { return string.Empty; }Заглушку следует писать так, чтобы тест не выполнялся - это поможет удостовериться, что тест правильно реагирует на ошибку. Другая, более важная задача "красной" фазы состоит в том, чтобы в процессе написания теста определить сигнатуры тестируемых методов, порядок их вызова и другие особенности работы с ними. Как правило, имеет смысл начинать тестирование метода с наиболее общей, дефолтной ситуации, которая в нашем случае заключается в том, что в метод подаются числа, выходящие за пределы заданного диапазона. Поэтому мы пишем тест, который закончится удачно только в том случае, если тестируемый метод выбросит исключение заданного типа.
В "зеленой" фазе мы имплементируем и отлаживаем наш метод. При наличии альтернатив имеет смысл использовать самую простую имплементацию. Если возникает необходимость написать код, который не проверяется тестом, необходимо вернуться в красную фазу и исправить тесты.
static public string GetDayOfWeekName(int dayNumber) { throw new ArgumentException(); }Добившись успешного выполнения всех написанных тестов, просматриваем код в поисках потенциально полезных рефакторингов.
Чаще всего в этой фазе приходится убирать дублируемый код и изменять имена переменных, методов и классов. Иногда в фазе рефакторинга не нужно делать ничего, кроме повторного просмотра написанного кода. В данном случае мы имеем дело именно с такой ситуацией.
После завершения цикла "red - green - refactor" его нужно повторить для следующего участка функциональности. Для нас это случай, когда числа попадают в нужный диапазон. Пишем несложный тест и проверяем, что он не работает. Assert в модульных тестах является стандартным способом документировать наши предположения.
[Test] public void TestGetDayOfWeekName() { Assert.AreEqual("Monday", Converter.GetDayOfWeekName(1)); ... Assert.AreEqual("Saturday", Converter.GetDayOfWeekName(7)); }
В "зеленой" фазе мы снова заставляем тест работать.
static public string GetDayOfWeekName(int dayNumber) { switch (dayNumber) { case 1: return "Monday"; ... case 7: return "Saturday"; } throw new ArgumentException(); }
В заключительной фазе цикла иногда приходится полностью менять имплементацию. В частности, в нашем случае хотелось бы снизить цикломатическую сложность кода за счет замены switch'а на хэш-таблицу. Запуск тестов после рефакторинга докажет, что ничего не было сломано.
static public string GetDayOfWeekName(int dayNumber) { Hashtable ht = new Hashtable(); ht[1] = "Monday"; ht[7] = "Saturday"; string result = ht[dayNumber] as string; if (result == null) throw new ArgumentException(); return result; }
Как правило, цикл "red - green - refactor" должен занимать от 5 до 20 минут, хотя исключения, конечно, встречаются. Не следует работать в нескольких фазах одновременно: рефакторить код, например, в "красной" фазе неразумно. Чем больше тестов отлаживается в одном цикле, тем хуже; в идеале нужно работать только с одним тестом одновременно.
Обычно программистам, впервые сталкивающимся с "Test First", этот стиль кажется значительно более трудоемким.
Доля правды в этом, безусловно, есть: для тестирования все-таки приходится писать дополнительный код. Давайте попробуем разобраться, что мы приобретаем взамен.
Ранее считалось, что модульное тестирование имеет только одну цель - уменьшение количества "багов". XP значительно усиливает роль тестирования и выделяет пять его основных функций:
- Традиционная: уменьшение количества "багов".
- Поддержка низкоуровневого дизайна.
- Поддержка рефакторинга.
- Поддержка отладки.
- Наконец, тест помогает документировать код.
Конечно, модульные тесты отлично выявляют такие виды "багов", как бесконечный цикл, невыход из рекурсии, присвоение в неправильную переменную и многие другие. Из опыта хорошо известно, что в коде, для которого тесты существуют, дефектов всегда значительно меньше. Но кое-что после них все равно остается. Поэтому модульное тестирование должно быть дополнительной, а не окончательной линией защиты от ошибок.
Модульное тестирование, при некотором навыке, играет важную роль в качестве средства поддержки дизайна. Чтобы написать тест, необходимо детально продумать интерфейс проектируемого класса и протокол его использования. Что еще более важно, тестирование в рамках TDD позволяет получить простой способ оценки полноты интерфейсов: необходимым и достаточным считается такой интерфейс, который позволяет выполнить все написанные тесты. Все, что находится за этими рамками, считается ненужным. Наконец, использование тестов в качестве инструмента дизайна заставляет программиста в первую очередь концентрироваться на интерфейсе, а уже во вторую - на имплементации, что также положительно влияет на результат.
Значение тестов для рефакторинга переоценить невозможно. Любой, даже самый небольшой рефакторинг, как известно, требует наличия написанных тестов. Эта мысль важна настолько, что Мартин Фаулер в своей книге "Refactoring" посвятил модульному тестированию целую главу. Конечно, некоторые виды рефакторингов, такие, как, например, переименование полей, вполне возможно применять и без тестов. Но набор модульных тестов, покрывающих большую часть приложения, позволяет модифицировать систему значительно более агрессивно и со значительно более предсказуемыми результатами. Именно сочетание рефакторинга и тестов позволяет XP-программистам быстро изменять систему в любом нужном заказчику направлении. И именно поэтому мы можем позволить себе не искать гибких решений, а использовать самые простые (цена изменений при наличии тестов не слишком высока).
Разработчики знают, как тяжело порой бывает отладить какой-нибудь метод, глубоко закопанный в большом приложении. Иногда не удается проверить только что написанный код, потому что он еще нигде и никак не используется. Часто воспроизведение тестовой ситуации занимает чересчур много времени. В некоторых ситуациях программисты даже разрабатывают специальные утилиты, позволяющие отладить тот или иной компонент отдельно от всей системы.
При использовании модульных тестов подобных проблем просто не возникает. Если нужно что-нибудь проверить - пишем тест. Тесты, вообще говоря, представляют собой практически идеальную отладочную среду. Они находятся полностью под контролем программиста и при правильном применении позволяют вызвать любой код в широком диапазоне условий.
Кроме того, модульное тестирование ведет к сокращению общих затрат времени на отладку. Для большинства ошибок, найденных при модульном тестировании, отладка вообще не требуется, их видно сразу. Даже сложные "баги" отлаживать становится проще, поскольку точно известны место их возникновения (код, который был написан только что) и условия воспроизведения (тест, который сейчас отлаживается).Искать те же самые ошибки в работающем приложении почти всегда оказывается значительно более сложным и долгим делом.