Автор книги: Кент Бек
Жанр: Зарубежная компьютерная литература, Зарубежная литература
Возрастные ограничения: +12
сообщить о неприемлемом содержимом
Текущая страница: 6 (всего у книги 17 страниц) [доступный отрывок для чтения: 6 страниц]
17. Ретроспектива денежного примера
Давайте еще раз окинем взглядом пример реализации мультивалютных вычислений и попробуем оценить использованный нами подход и полученные результаты. Вот несколько тезисов, которых хотелось бы коснуться:
□ Что дальше? Как определить дальнейшее направление разработки?
□ Метафора. Впечатляющий эффект, который метафора оказывает на структуру дизайна.
□ Использование JUnit. Как часто мы запускали тесты и как мы использовали JUnit?
□ Метрики кода. Численные характеристики получившегося кода.
□ Процесс. Мы говорим: «красный—зеленый—рефакторинг». Как много усилий прикладывается на каждом из этих этапов?
□ Качество тестов. Каким образом характеристики тестов TDD соотносятся с характеристиками обычных методик тестирования?
Что дальше?Можно ли считать код завершенным? Нет. Методы Sum.plus() и Money.plus() во многом дублируют друг друга. Если мы преобразуем интерфейс Expression в класс (не совсем обычное преобразование – чаще классы становятся интерфейсами), мы получим возможность переместить общий код в единый общий метод.
На самом деле мне сложно представить себе код, который полностью завершен. Методику TDD можно использовать как способ приближения к идеалу, однако это будет не самое эффективное ее использование. Если вы имеете дело с крупной системой, тогда части системы, с которыми вы работаете каждый день, должны быть тщательно «вылизаны». Иными словами, дизайн должен быть чистым и понятным, а код должен быть хорошо протестированным. В этом случае вы изо дня в день сможете вносить в систему необходимые изменения, не теряя при этом уверенности в работоспособности кода. Однако на периферии системы, где располагаются части, к которым вы обращаетесь относительно редко, количество тестов может быть меньше, а дизайн – уродливее.
Когда я завершаю решение всех очевидных задач, я люблю запускать инструмент проверки оформления кода (например, SmallLint для Smalltalk). Многие полученные в результате этого предложения мне и без того известны. Со многими предложениями я не согласен. Однако автоматизированный инструмент проверки кода ни о чем не забывает, поэтому иногда он обнаруживает то, что было упущено мною из виду.
Еще один полезный вопрос: «Какие дополнительные тесты необходимо написать для системы?» Иногда кажется, что некоторый тест должен потерпеть неудачу, однако, добавив его в тестовый набор, вы обнаруживаете, что он работает. В этом случае необходимо определить, почему так происходит. Иногда тест, который не должен работать, действительно не работает, и вы добавляете его в набор, как признак известного вам ограничения разрабатываемой системы или как напоминание о работе, которую необходимо выполнить позднее.
Наконец, когда список задач пуст, неплохо еще раз проверить дизайн. Удовлетворяет ли данная реализация всем предъявляемым требованиям? Существует ли дублирование, которое сложно устранить при использовании данного дизайна? (Сохранившееся дублирование – признак нереализованного дизайна.)
МетафораЛично для меня самым большим сюрпризом в данном примере явилось впечатляющее отличие окончательного дизайна от тех разработок, с которыми мне приходилось иметь дело до написания этой книги. Я выполнял разработку аналогичного мультивалютного кода для различных программных систем, реально используемых в производстве, по меньшей мере три раза (насколько я могу припомнить). Кроме того, я использовал эту же задачу для разного рода публикаций еще раз шесть или семь. Помимо публикаций я пятнадцать раз программировал этот пример перед аудиторией на различных конференциях (программировал со сцены – звучит здорово, но выглядит менее впечатляюще). Наконец, прежде чем написать окончательный вариант первой части данной книги, я перебрал три или четыре различных направления разработки кода (я менял направление своих мыслей в соответствии с поступавшими ранними рецензиями и отзывами о написанном материале). И вот, пока я работал над текстом первой части, мне в голову пришла мысль использовать в качестве метафоры математические выражения (expressions). В результате дизайн стал развиваться по совершенно иному, не известному мне ранее пути.
Я никогда не думал, что метафора – это настолько мощный инструмент. Многие думают, что метафора – это всего лишь источник имен. Разве не так? Похоже, что нет.
Для представления «комбинации нескольких денежных величин, которые могут быть выражены в разных валютах», Уорд Каннингэм использовал метафору вектора. Имеется в виду математический вектор – набор коэффициентов, каждому из которых соответствует некоторая валюта. Лично я некоторое время использовал метафору суммы денег (MoneySum), затем придумал денежный мешок (MoneyBag) – звучит понятно и близко к реальности, – наконец, остановился на метафоре бумажника (Wallet). Что такое бумажник и как он функционирует, известно абсолютно всем. Все эти метафоры подразумевают, что набор денежных значений (объектов Money) является плоским. Иначе говоря, выражение 2 USD + 5 CHF + 3 USD эквивалентно выражению 5 USD + 5 CHF. Два значения в одной и той же валюте автоматически сливаются в одно.
Метафора математического выражения избавила меня от множества неприятных проблем, связанных со слиянием дублирующихся валют. Результирующий код получился чище, чем я когда-либо видел. Конечно же, я несколько обеспокоен производительностью кода, основанного на подобной метафоре, однако, прежде чем приступать к оптимизации, я намерен проанализировать статистику обращений к различным участкам кода.
Почему я был вынужден переписать заново то, что я уже писал до этого не меньше 20 раз? Буду ли я и дальше сталкиваться с подобными сюрпризами? Существует ли способ, который позволит мне найти правильное решение, по крайней мере в течение первых трех попыток? А может быть, этот способ позволит мне найти правильное решение с первой попытки?
Использование JUnitЯ поручил инфраструктуре JUnit вести журнал в процессе разработки мультивалютного примера. Выяснилось, что за все время я нажал клавишу Enter ровно 125 раз. Оценку интервала между запусками тестов нельзя считать достоверной, так как в ходе работы я не только программировал, но и писал текст книги. Однако когда я занимался только программированием, я запускал тесты приблизительно раз в минуту.
На рис. 17.1 представлена гистограмма интервалов между запусками тестов. Большое количество длительных интервалов, скорее всего, обусловлено тем, что я тратил значительное время на написание текста книги.
Рис. 17.1. Гистограмма интервалов времени между запусками тестов
Метрики кода
В табл. 17.1 приводятся некоторые статистические данные, характеризующие код.
Таблица 17.1. Метрики кода
Вот некоторые примечания к данной таблице:
1. Мы не реализовали весь программный интерфейс (API) целиком, поэтому не можем достоверно оценить полное количество функций, или количество функций на один класс, или количество строк кода на один класс. Однако соотношения этих параметров можно считать поучительными. Количество функций и количество строк в тестах приблизительно такое же, как и в функциональном коде.
2. Количество строк кода в тестах можно сократить, если извлечь из кода операции подготовки тестовых данных. Однако общее соотношение между строками функционального кода и строками тестирующего кода при этом сохранится.
3. Цикломатическая сложность (cyclomatic complexity) – это величина, характеризующая сложность обычного потока управления в программе. Цикломатическая сложность тестов равна 1, так как в тестирующем коде нет ни ветвлений, ни циклов. Цикломатическая сложность функционального кода близка к единице, так как вместо явных ветвлений для передачи управления чаще используется полиморфизм.
4. Оценка количества строк в функции дана с учетом заголовка функции и закрывающей скобки.
5. Количество строк на функцию для тестирующего кода в нашем случае больше чем могло бы быть, так как мы не выделили общий код в отдельные функции. Об этом рассказывается в главе 29, которая посвящена методам работы с xUnit.
ПроцессЦикл TDD выглядит следующим образом:
□ написать тест;
□ запустить все тесты и убедиться, что добавленный тест терпит неудачу;
□ внести в код изменения;
□ запустить тесты и убедиться, что все они выполнились успешно;
□ выполнить рефакторинг, чтобы устранить дублирование.
Если исходить из того, что разработка теста – это один шаг, какое количество изменений требуется сделать, чтобы выполнить компиляцию, запуск и рефакторинг? (Под изменением я подразумеваю изменение определения метода или класса.) На рис. 17.2 показана гистограмма количества изменений для каждого из тестов «денежного» примера, над которым мы работали в первой части книги.
Рис. 17.2. Гистограмма количества изменений, приходящихся на каждый период рефакторинга
Я полагаю, что если бы мы собирали статистику для достаточно крупного проекта, мы обнаружили бы, что количество изменений, необходимых для компиляции и запуска кода, очень невелико (это количество можно уменьшить, если среда разработки будет понимать, что пытаются ей сказать тесты, и, например, автоматически добавлять в функциональный код необходимые заглушки). Однако количество изменений, вносимых в код во время рефакторинга, должно соответствовать (вот главный тезис) кривой распределения с эксцессом больше нормального, то есть с большим числом изменений, чем предсказывается стандартной кривой нормального распределения. Подобный профиль характерен для многих других естественных процессов, например для изменения стоимости акций на рынке ценных бумаг99
Fractals and Scaling in Finance / Benoit Mandelbrot, editor. SpringerVerlag, 1997. ISBN: 0387983635
[Закрыть].
Тесты являются неотъемлемой частью методики TDD. Они могут запускаться в любое время работы над программой, а также после того, как программа будет завершена. Однако не стоит путать их с другими важными типами тестирования:
□ тестированием производительности;
□ нагрузочным тестированием;
□ тестированием удобства использования.
Тем не менее, если плотность вероятности дефектов в коде, разработанном с использованием TDD, невелика, роль профессионального тестирования меняется. Если обычно профессиональное тестирование используется для постоянного надзора за работой программистов, то при использовании TDD профессиональное тестирование больше напоминает вспомогательный инструмент, облегчающий коммуникацию между теми, кто знает, как должна работать система, и теми, кто создает систему.
Как можно оценить качество разработанных нами тестов? Вот два широко распространенных метода:
Охват кода (statement coverage). Для оценки качества тестов этой характеристики недостаточно, однако ее можно использовать как отправную точку. Если программист ревностно следует всем требованиям TDD, тесты должны охватывать 100% кода. Для оценки этой характеристики можно использовать специальные программные средства. Например, программа JProbe (www.sitaka.com/software/jprobe) сообщает нам, что в нашем примере не охваченной тестами осталась всего одна строка в одном методе – Money.toString(). Напомню, что эта строка была добавлена в отладочных целях, фактически она не является функциональным кодом.
Намеренное добавление дефекта (defect insertion). Это еще один способ проверки качества тестов. Идея проста: изменить значение строки кода и убедиться, что тест перестал работать. Делать это можно вручную или при помощи специального инструмента, такого как Jester (jester.sourceforge.net). Этот инструмент сообщает нам, что в нашей программе существует всего одна строка, которую можно изменить, не нарушив работы тестов. Вот эта строка: Pair.hashCode(). Здесь мы просто подделали реализацию – вместо хеш-кода метод возвращает постоянное значение: 0. Если одно постоянное значение заменить другим, смысл программы не изменится (одна подделка ничем не лучше другой), поэтому подобную модификацию кода нельзя считать дефектом.
Флип, один из рецензентов моей книги, сообщил мне некоторые дополнительные соображения относительно охвата тестами. Абсолютный показатель охвата вычисляется следующим образом: количество тестов, предназначенных для тестирования различных аспектов программы, необходимо разделить на количество аспектов, которые нуждаются в тестировании (сложность логики программы). Существует два способа улучшить показатель охвата тестами. Во-первых, можно написать больше тестов. Отсюда разница в количестве тестов, которые пишутся разработчиком, использующим TDD, и профессиональным тестером. (В главе 32 приводится пример задачи, для решения которой я написал 6 тестов, а человек, профессионально занимающийся тестированием, – 65 тестов.) Однако существует и другой способ улучшить охват – ограничиться фиксированным набором тестов и упростить логику программы. Подобный эффект зачастую достигается в процессе рефакторинга – условные операторы заменяются сообщениями классов или вовсе удаляются из программы. Флип выражает эту мысль так: «Вместо того чтобы увеличить количество тестов и тем самым охватить всевозможные комбинации входных данных (говоря точнее, эффективное подмножество всех комбинаций), мы оставляем количество тестов неизменным и меняем количество внутренних структурных комбинаций кода».
Последний взгляд назадСуществует три важных навыка, которые необходимо освоить тем, кто впервые изучает TDD:
□ три основных подхода, которые используются, чтобы заставить тест работать: подделка реализации, триангуляция и очевидная реализация;
□ устранение дублирования между функциональным кодом и тестами – важный способ формирования дизайна;
□ способность контролировать расстояние между тестами: когда дорога становится скользкой, необходимо двигаться маленькими шажками; когда дальнейший путь ясен, можно увеличить скорость.
Часть II
На примере xUnit
Какой подход использовать при создании инструмента для разработки через тестирование? Естественно, разработку через тестирование.
Архитектура xUnit хорошо реализуется на языке Python, поэтому во второй части книги я перейду на использование Python. Не беспокойтесь, для тех, кто никогда раньше не имел дела с Python, я добавлю в текст необходимые пояснения. Когда вы прочитаете вторую часть, вы, во-первых, освоите базовые навыки программирования на Python, во-вторых, узнаете, как самому разработать свою собственную инфраструктуру для автоматического тестирования, и, в-третьих, ознакомитесь с более сложным примером использования методики TDD – три по цене одного!
18. Первые шаги на пути к xUnit
Разработка инструмента тестирования с использованием самого этого инструмента для тестирования многим может показаться чем-то, напоминающим хирургическую операцию на своем собственном мозге. («Только не вздумай трогать центры моторики! О! Слишком поздно! Игра окончена».) Сначала эта идея может показаться жутковатой. Однако инфраструктура тестирования обладает более сложной внутренней логикой, если сравнивать с относительно несложным денежным примером, рассмотренным в первой части книги. Часть II можно рассматривать как шаг в сторону разработки «настоящего» программного обеспечения. Кроме того, вы можете рассматривать этот материал как упражнение в самодокументируемом программировании.
Прежде всего, у нас должна быть возможность создать тест и запустить тестовый метод. Например: TestCase("testMethod").run(). Возникает проблема: мы собираемся написать тест для программного кода, который мы будем использовать для написания тестов. Так как у нас пока еще нет даже намека на инфраструктуру тестирования, мы вынуждены проверить правильность нашего самого первого шага вручную. К счастью, мы достаточно хорошо отдохнули, а значит, вероятность того, что мы допустим ошибку, относительно невелика. Однако чтобы сделать ее еще меньше, мы планируем двигаться маленькими-маленькими шажками, тщательно проверяя все, что мы делаем. Вот список задач, который приходит на ум, когда начинаешь размышлять о разработке собственной инфраструктуры тестирования:
Вызов тестового метода
Вызов метода setUp перед обращением к методу
Вызов метода tearDown после обращения к методу
Метод tearDown должен вызываться даже в случае неудачи теста
Выполнение нескольких тестов
Отчет о результатах
Конечно же, мы по-прежнему работаем в стиле «сначала тесты». Для нашего прототеста нам потребуется небольшая программа, которая должна отображать на экране значение «истина», если произошло обращение к тестовому методу, и значение «ложь» в противном случае. Теперь представим, что у нас есть тест, который устанавливает флаг внутри тестового метода, в этом случае мы могли бы после выполнения теста отобразить состояние флага на экране и самостоятельно убедиться в том, что флаг установлен правильно. Выполнив проверку вручную, мы сможем попробовать автоматизировать процесс.
Итак, у нас наметилась следующая стратегия. Мы создаем объект, который соответствует нашему тесту. В объекте содержится флаг. Перед выполнением тестового метода флаг должен быть установлен в состояние «ложь». Тестовый метод устанавливает флаг в состояние «истина». После выполнения тестового метода мы должны проверить состояние флага. Назовем наш тестовый класс именем WasRun1010
В переводе с английского языка was run означает был выполнен. – Примеч. пер.
[Закрыть], так как объект этого класса будет сигнализировать нам о том, был ли выполнен тестовый метод. Флаг внутри этого класса также будет называться wasRun (это несколько сбивает с толку, однако wasRun – такое подходящее имя). Собственно объект (экземпляр класса WasRun) будет называться просто test. То есть мы сможем написать инструкцию assert test.wasRun (assert – встроенная инструкция языка Python).
Язык программирования Python является интерпретируемым – команды исполняются по мере чтения их из файла с исходным кодом. Поэтому, чтобы выполнить тестирование, мы можем написать следующий короткий файл и попробовать запустить его:
test = WasRun(«testMethod»)
print(test.wasRun)
test.testMethod()
print(test.wasRun)
Мы ожидаем, что эта миниатюрная программа напечатает None до выполнения тестового метода и 1 – после. (В языке Python значение None является аналогом null или nil и наряду с числом 0 соответствует значению «ложь».) Однако программа не делает того, что мы от нее ждем. И немудрено – мы еще не определили класс WasRun (сначала тесты!).
WasRun
class WasRun:
pass
(Ключевое слово pass используется в случае, если реализация класса или метода отсутствует.) Теперь интерпретатор сообщает нам, что в классе WasRun нет атрибута с именем wasRun. Создание атрибута происходит в момент создания объекта (экземпляра класса), то есть в процессе выполнения конструктора (для удобства конструктор любого класса называется __init__). Внутри конструктора мы присваиваем флагу wasRun значение None (ложь):
WasRun
class WasRun:
def __init__(self, name):
self.wasRun = None
Теперь программа действительно отображает на экране значение None, однако после этого интерпретатор сообщает нам, что мы должны определить в классе WasRun метод testMethod. (Было бы неплохо, если бы среда разработки автоматически реагировала на это: самостоятельно создавала бы функцию-заглушку и открывала редактор с курсором, установленным в теле этой функции. Не правда ли, это было бы просто здорово? Кстати, некоторые производители IDE уже додумались до этого.)
WasRun
def testMethod(self):
pass
Запускаем файл и видим на экране два значения: None и None1111
В языке Python заголовок определения метода (функции) начинается со служебного слова def, а завершается двоеточием. Операторы тела метода записываются ниже, в отдельных строчках. Группировка операторов тела определяется отступами (вместо фигурных скобок). – Примеч. ред.
[Закрыть]. Нам хотелось бы видеть None и 1. Чтобы получить желаемый результат, в теле метода testMethod присвоим флагу wasRun желаемое значение:
WasRun
def testMethod(self):
self.wasRun = 1
Запускаем программу – то, что нужно! Мы получили желаемый результат. Зеленая полоса – ур-р-ра! Нам предстоит сложный рефакторинг, однако если мы видим перед собой зеленую полосу, значит, мы добились прогресса.
Теперь, вместо того чтобы напрямую обращаться к нашему тестовому методу, мы должны использовать наш реальный интерфейс – метод run(). Изменим тест следующим образом:
test= WasRun(«testMethod»)
print(test.wasRun)
test.run()
print(test.wasRun)
Чтобы заставить тест работать, достаточно воспользоваться следующей несложной реализацией:
WasRun
def run(self):
self.testMethod()
Наша тестовая программа снова печатает на экране то, что нам нужно. Зачастую во время рефакторинга возникает ощущение, что необходимо разделить код, с которым вы работаете, на две части, чтобы работать с ними по отдельности. Если в конце работы они снова сольются воедино, – замечательно. Если нет, значит, вы можете оставить их отдельно друг от друга. В данном случае со временем мы планируем создать класс TestCase, однако вначале мы должны обособить части нашего примера.
Следующий этап – динамический вызов метода testMethod. Одной из приятных отличительных характеристик языка Python является возможность использования имен классов и методов в качестве функций (см. создание экземпляра класса WasRun). Получив атрибут, соответствующий имени теста, мы можем обратиться к нему, как к функции. В результате будет выполнено обращение к методу с соответствующим именем1212
Спасибо Дункану Бусу (Duncan Booth) за то, что он исправил допущенную мной ошибку, типичную для малоопытных программистов на Python, и подсказавшему мне решение, в большей степени соответствующее этому языку.
[Закрыть].
WasRun
class WasRun: def __init__(self, name):
self.wasRun = None
self.name = name
def run(self):
method = getattr(self, self.name)
method()
Это еще один шаблон рефакторинга: разработать код, который работает с некоторым конкретным экземпляром, и обобщить его, чтобы он мог работать со всеми остальными экземплярами, для этого константы заменяются переменными. В данном случае роль константы играет не некоторое значение, а фиксированный код (имя конкретного метода). Однако принцип остается тем же. В рамках TDD эта проблема решается очень легко: методика TDD снабжает вас конкретными работающими примерами, исходя из которых можно выполнить обобщение. Это значительно проще, чем выполнять обобщение исходя только из собственных умозаключений.
Теперь наш маленький класс WasRun занят решением двух разных задач: во-первых, он следит за тем, был ли выполнен метод; во-вторых, он динамически вызывает метод. Пришло время разделить полномочия (разделить нашу работу на две разные части). Прежде всего, создадим пустой суперкласс TestCase и сделаем класс WasRun производным классом:
TestCase
class TestCase: pass
WasRun
class WasRun(TestCase): . . .
Теперь переместим атрибут name из подкласса в суперкласс:
TestCase
def __init__(self, name):
self.name = name
WasRun
def __init__(self, name):
self.wasRun = None
TestCase.__init__(self, name)
Наконец, замечаем, что метод run() использует только атрибуты суперкласса, значит, скорее всего, он должен располагаться в суперклассе. (Я всегда стараюсь размещать операции рядом с данными.)
TestCase
def __init__(self, name):
self.name= name
def run(self):
method = getattr(self, self.name)
method()
Естественно, между выполнениями этих модификаций я каждый раз запускаю тесты, чтобы убедиться, что все работает как надо.
Нам надоело смотреть на то, как наша программа каждый раз печатает одно и то же: None и 1. Использовав разработанный механизм, мы можем теперь написать:
TestCaseTest
class TestCaseTest(TestCase):
def testRunning(self):
test = WasRun("testMethod")
assert(not test.wasRun)
test.run()
assert(test.wasRun)
TestCaseTest("testRunning").run()
Вызов тестового метода
Вызов метода setUp перед обращением к методу
Вызов метода tearDown после обращения к методу
Метод tearDown должен вызываться даже в случае неудачи теста
Выполнение нескольких тестов
Отчет о результатах
Сердцем этого теста являются операторы print, превращенные в выражения assert, таким образом, вы можете видеть, что выполненная нами процедура – это усложненный шаблон рефакторинга «Выделение метода» (Extract Method).
Я открою вам маленький секрет. Шажки, которыми мы двигались от теста к тесту в данной главе, выглядят смехотворно маленькими. Однако до того, как получить представленный результат, я пытался выполнить разработку более крупными шагами и потратил на это около шести часов (конечно, мне пришлось тратить дополнительное время на изучение тонкостей языка Python). Я два раза начинал с нуля и каждый раз думал, что мой код сработает, однако этого не происходило. Я понял, что попытка пропустить самый начальный, самый примитивный этап разработки, – это грубейшее нарушение принципов TDD.
Конечно же, вы не обязаны постоянно перемещаться такими лилипутскими шажками. После того как вы освоите TDD, вы сможете двигаться вперед более уверенно, реализуя между тестами значительно больший объем функциональности. Однако чтобы в совершенстве освоить TDD, вы должны научиться перемещаться маленькими шажками тогда, когда это необходимо.
Далее мы планируем перейти к решению задачи обращения к методу setUp(). Однако вначале подведем итог.
В данной главе мы
□ поняли, как начать работу со смехотворно малюсенького шага;
□ реализовали функциональность путем создания фиксированного кода, а затем обобщения этого кода путем замены констант на переменные;
□ использовали шаблон «Встраиваемый переключатель» (Pluggable Selector) и дали себе обещание не использовать его вновь в течение как минимум четырех месяцев, так как он существенно усложняет анализ кода;
□ начали работу над инфраструктурой тестирования маленькими шажками.
Внимание! Это не конец книги.
Если начало книги вам понравилось, то полную версию можно приобрести у нашего партнёра - распространителя легального контента. Поддержите автора!Правообладателям!
Данное произведение размещено по согласованию с ООО "ЛитРес" (20% исходного текста). Если размещение книги нарушает чьи-либо права, то сообщите об этом.Читателям!
Оплатили, но не знаете что делать дальше?