Создание, анализ ирефакторинг
Скачать 3.16 Mb.
|
149 cationsController . Кроме того, сразу же после появления TransmitterAPI можно создать граничные тесты для проверки правильности использования API . Рис . 8 .2 . Прогнозирование интерфейса передатчика Чистые границы На границах происходит много интересного . В частности, стоит уделить особое внимание изменениям . В хорошей программной архитектуре внесение изменений обходится без значительных затрат и усилий по переработке . Если в продукте используется код, находящийся вне нашего контроля, примите особые меры по защите капиталовложений и позаботьтесь о том, чтобы будущие изменения обходились не слишком дорого . Для граничного кода необходимо четкое разделение сторон и тесты, опреде- ляющие ожидания пользователя . Постарайтесь, чтобы ваш код поменьше знал о специфических подробностях реализации стороннего кода . Лучше зависеть от того, что находится под вашим контролем, чем от тех факторов, которые вы не контролируете (а то, чего доброго, они начнут контролировать вас) . Чтобы границы со сторонним кодом не создавали проблем в наших проектах, мы сводим к минимуму количество обращений к ним . Для этого можно восполь- зоваться обертками, как в примере с Map , или реализовать паттерн АДАПТЕР для согласования нашего идеального интерфейса с реальным, полученным от разработчиков . В обоих вариантах код становится более выразительным, обе- спечивается внутренняя согласованность обращений через границы, а изменение стороннего кода требует меньших затрат на сопровождение . литература [BeckTDD]: Test Driven Development, Kent Beck, Addison-Wesley, 2003 . [GOF]: Design Patterns: Elements of Reusable Object Oriented Software, Gamma et al ., Addison-Wesley, 1996 . [WELC]: Working Effectively with Legacy Code, Addison-Wesley, 2004 . 149 Fake Модульные тесты За последние десять лет наша профессия прошла долгий путь . В 1997 году никто не слыхал о методологии TDD (Test Driven Development, то есть «разработка че- рез тестирование») . Для подавляющего большинства разработчиков модульные тесты представляли собой короткие фрагменты временного кода, при помощи которого мы убеждались в том, что наши программы «работают» . Мы тщательно выписывали свои классы и методы, а потом подмешивали специализированный код для их тестирования . Как правило, при этом использовалась какая-нибудь несложная управляющая программа, которая позволяла вручную взаимодейство- вать с тестируемым кодом . Помню, в середине 90-х я написал программу на C++ для встроенной системы реального времени . Программа представляла собой простой таймер со следующей сигнатурой: void Timer::ScheduleCommand(Command* theCommand, int milliseconds) 9 150 Три закона TTD 151 Идея была проста; метод Execute класса Command выполнялся в новом программ- ном потоке с заданной задержкой в миллисекундах . Оставалось понять, как его тестировать . Я соорудил простую управляющую программу, которая прослушивала события клавиатуры . Каждый раз, когда на клавиатуре вводился символ, программа планировала выполнение команды, повторяющей этот же символ пять секунд спустя . Затем я настучал на клавиатуре ритмичную мелодию и подождал, пока эта мелодия «появится» на экране спустя пять секунд . «Мне… нужна такая девушка… как та… которую нашел мой старый добрый папа…» Я напевал эту мелодию, нажимая клавишу « .», а потом пропел ее снова, когда точки начали появляться на экране . И это был весь тест! Я убедился в том, что программа работает, показал ее своим коллегам и выкинул тестовый код . Как я уже говорил, наша профессия прошла долгий путь . Сейчас я бы написал комплексный тест, проверяющий, что все углы и закоулки моего кода работают именно так, как положено . Я бы изолировал свой код от операционной системы, не полагаясь на стандартное выполнение по таймеру . Я бы самостоятельно реа- лизовал хронометраж, чтобы тестирование проходило под моим полным контро- лем . Запланированные команды устанавливали бы логические флаги, а потом тестовый код выполнял бы мою программу в пошаговом режиме, наблюдая за состоянием флагов и их переходами из ложного состояния в истинное по про- хождении нужного времени . Когда у меня накопился бы пакет тестов, я бы позаботился о том, чтобы эти тесты были удобными для любого другого программиста, которому потребуется работать с моим кодом . Я бы проследил за тем, чтобы тесты и код поставлялись вместе, в одном исходном пакете . Да, мы прошли долгий путь; но дорога еще не пройдена до конца . Движения гибких методологий и TDD поощряют многих программистов писать автома- тизированные модульные тесты, а их ряды ежедневно пополняются новыми сторонниками . Однако в лихорадочном стремлении интегрировать тестирование в свою работу многие программисты упускают более тонкие и важные аспекты написания хороших тестов . три закона TTD В наши дни каждому известно, что по требованиям методологии TDD модуль- ные тесты должны писаться заранее, еще до написания кода продукта . Но это правило — всего лишь верхушка айсберга . Рассмотрим следующие три закона 1 : 1 Professionalism and Test-Driven Development, Robert C . Martin, Object Mentor, IEEE Software, May/June 2007 (Vol . 24, No . 3), pp . 32–36; http://doi .ieeecomputersociety .org/10 .1109/MS .2007 .85 151 152 Глава 9 . Модульные тесты Первый закон. Не пишите код продукта, пока не напишете отказной модульный тест . Второй закон. Не пишите модульный тест в объеме большем, чем необходимо для отказа . Невозможность компиляции является отказом . Третий закон. Не пишите код продукта в объем большем, чем необходимо для прохождения текущего отказного теста . Эти три закона устанавливают рамки рабочего цикла, длительность которого составляет, вероятно, около 30 секунд . Тесты и код продукта пишутся вместе, а тесты на несколько секунд опережают код продукта . При такой организации работы мы пишем десятки тестов ежедневно, сотни те- стов ежемесячно, тысячи тестов ежегодно . При такой организации работы тесты охватывают практически все аспекты кода продукта . Громадный объем тестов, сравнимый с объемом самого кода продукта, может создать немало организаци- онных проблем . О чистоте тестов Несколько лет назад мне предложили заняться обучением группы, которая реши- ла, что тестовый код не должен соответствовать тем же стандартам качества, что и код продукта . Участники группы сознательно разрешили друг другу нарушать правила в модульных тестах . «На скорую руку» — вот каким девизом они руко- водствовались . Разумно выбирать имена переменных не обязательно, короткие и содержательные тестовые функции не обязательны . Качественно проектировать тестовый код, организовать его продуманное логическое деление не обязательно . Тестовый код работает, охватывает код продукта — и этого вполне достаточно . Пожалуй, некоторые читатели сочувственно отнесутся к этому решению . Воз- можно, кто-то в прошлом писал тесты наподобие тех, которые я написал для своего класса Timer . Примитивные «временные» тесты отделены огромным рас- стоянием от пакетов автоматизированного модульного тестирования . Многие программисты (как и та группа, в которой я преподавал) полагают, что тесты «на скорую руку» — лучше, чем полное отсутствие тестов . Но на самом деле тесты «на скорую руку» равносильны полному отсутствию тестов, если не хуже . Дело в том, что тесты должны изменяться по мере развития кода продукта . Чем примитивнее тесты, тем труднее их изменять . Если тестовый код сильно запутан, то может оказаться, что написание нового кода продукта займет меньше времени, чем попытки втиснуть новые тесты в обновленный пакет . При изменении кода продукта старые тесты перестают проходить, а неразбериха в тестовом коде не позволяет быстро разобраться с возникшими проблемами . Та- ким образом, тесты начинают рассматриваться как постоянно растущий балласт . От версии к версии затраты на сопровождение тестового пакета непрерывно росли . В конечном итоге тесты стали главной причиной для жалоб разработчи- 152 О чистоте тестов 153 ков . Когда руководство спрашивало, почему работа занимает столько времени, разработчики винили во всем тесты . Кончилось все тем, что они полностью от- казались от тестового пакета . Однако без тестов программисты лишились возможности убедиться в том, что изменения в кодовой базе работают так, как ожидалось . Без тестов они уже не могли удостовериться в том, что изменения в одной части системы не нарушают работу других частей . Количество ошибок стало возрастать . А с ростом количе- ства непредвиденных дефектов программисты начали опасаться изменений . Они перестали чистить код продукта, потому что боялись: не будет ли от изменений больше вреда, чем пользы? Код продукта стал загнивать . В итоге группа осталась без тестов, с запутанной и кишащей ошибками кодовой базой, с недовольными клиентами и с чувством, что все усилия по тестированию не принесли никакой пользы . И в определенном смысле они были правы . Их усилия по тестированию действи- тельно оказались бесполезными . Но виной всему было их решение — небрежно написанные тесты привели к катастрофе . Если бы группа ответственно подошла к написанию тестов, то затраченное время не пропало бы даром . Я говорю об этом вполне уверенно, потому что работал (и преподавал) во многих группах, добившихся успеха с аккуратно написанными модульными тестами . Мораль проста: тестовый код не менее важен, чем код продукта . Не считайте его «кодом второго сорта» . К написанию тестового кода следует относиться вдум- чиво, внимательно и ответственно . Тестовый код должен быть таким же чистым, как и код продукта . тесты как средство обеспечения изменений Если не поддерживать чистоту своих тестов, то вы их лишитесь . А без тестов утрачивается все то, что обеспечивает гибкость кода продукта . Да, вы не ошиб- лись . Именно модульные тесты обеспечивают гибкость, удобство сопровождения и возможность повторного использования нашего кода . Это объясняется просто: если у вас есть тесты, вы не боитесь вносить изменения в код! Без тестов любое изменение становится потенциальной ошибкой . Какой бы гибкой ни была ваша архитектура, каким бы качественным ни было логическое деление вашей архи- тектуры, без тестов вы будете сопротивляться изменениям из опасений, что они приведут к появлению скрытых ошибок . С тестами эти опасения практически полностью исчезают . Чем шире охват тестирования, тем меньше вам приходится опасаться . Вы можете практически свободно вносить изменения даже в имеющий далеко не идеальную архитектуру, запутанный и малопонятный код . Таким образом, вы можете спокойно улучшать архитектуру и строение кода! Итак, наличие автоматизированного пакета модульных тестов, охватывающих код продукта, имеет важнейшее значение для чистоты и ясности архитектуры . 153 154 Глава 9 . Модульные тесты А причина заключается в том, что тесты обеспечивают возможность внесения изменения . Таким образом, если ваши тесты недостаточно чисты и проработаны, ваши воз- можности по изменению кода сокращаются и вы лишаетесь возможности улуч- шения структуры кода . Некачественные тесты приводит к некачественному коду продукта . В конечном итоге тестирование вообще становятся невозможным, и код продукта начинает загнивать . Чистые тесты Какими отличительными признаками характеризуется чистый тест? Тремя: удо- бочитаемостью, удобочитаемостью и удобочитаемостью . Вероятно, удобочитае- мость в модульных тестах играет еще более важную роль, чем в коде продукта . Что делает тестовый код удобочитаемым? То же, что делает удобочитаемым любой другой код: ясность, простота и выразительность . В тестовом коде не- обходимо передать максимум информации минимумом выразительных средств . В листинге 9 .1 приведен фрагмент кода из проекта FitNesse . Эти три теста труд- ны для понимания; несомненно, их можно усовершенствовать . Прежде всего, повторные вызовы addPage и assertSubString содержат огромное количество по- вторяющегося кода [G5] . Что еще важнее, код просто забит второстепенными подробностями, снижающими выразительность теста . листинг 9 .1 . SerializedPageResponderTest.java public void testGetPageHieratchyAsXml() throws Exception { crawler.addPage(root, PathParser.parse("PageOne")); crawler.addPage(root, PathParser.parse("PageOne.ChildOne")); crawler.addPage(root, PathParser.parse("PageTwo")); request.setResource("root"); request.addInput("type", "pages"); Responder responder = new SerializedPageResponder(); SimpleResponse response = (SimpleResponse) responder.makeResponse( new FitNesseContext(root), request); String xml = response.getContent(); assertEquals("text/xml", response.getContentType()); assertSubString(" assertSubString(" assertSubString(" } public void testGetPageHieratchyAsXmlDoesntContainSymbolicLinks() throws Exception 154 Чистые тесты 155 { WikiPage pageOne = crawler.addPage(root, PathParser.parse("PageOne")); crawler.addPage(root, PathParser.parse("PageOne.ChildOne")); crawler.addPage(root, PathParser.parse("PageTwo")); PageData data = pageOne.getData(); WikiPageProperties properties = data.getProperties(); WikiPageProperty symLinks = properties.set(SymbolicPage.PROPERTY_NAME); symLinks.set("SymPage", "PageTwo"); pageOne.commit(data); request.setResource("root"); request.addInput("type", "pages"); Responder responder = new SerializedPageResponder(); SimpleResponse response = (SimpleResponse) responder.makeResponse( new FitNesseContext(root), request); String xml = response.getContent(); assertEquals("text/xml", response.getContentType()); assertSubString(" assertSubString(" assertSubString(" assertNotSubString("SymPage", xml); } public void testGetDataAsHtml() throws Exception { crawler.addPage(root, PathParser.parse("TestPageOne"), "test page"); request.setResource("TestPageOne"); request.addInput("type", "data"); Responder responder = new SerializedPageResponder(); SimpleResponse response = (SimpleResponse) responder.makeResponse( new FitNesseContext(root), request); String xml = response.getContent(); assertEquals("text/xml", response.getContentType()); assertSubString("test page", xml); assertSubString(" Например, присмотритесь к вызовам PathParser , преобразующим строки в экземпля- ры PagePath , используемые обходчиками (crawlers) . Это преобразование абсолютно несущественно для целей тестирования и только затемняет намерения автора . Второстепенные подробности, окружающие создание ответчика, а также сбор и пре- образование ответа тоже представляют собой обычный шум . Также обратите внима- ние на неуклюжий способ построения URL-адреса запроса из ресурса и аргумента . (Я участвовал в написании этого кода, поэтому считаю, что вправе критиковать его .) 155 156 Глава 9 . Модульные тесты В общем, этот код не предназначался для чтения . На несчастного читателя об- рушивается целый водопад мелочей, в которых необходимо разобраться, чтобы уловить в тестах хоть какой-то смысл . Теперь рассмотрим усовершенствованные тесты в листинге 9 .2 . Они делают абсолютно то же самое, но код был переработан в более ясную и выразительную форму . листинг 9 .2 . SerializedPageResponderTest.java (переработанная версия) public void testGetPageHierarchyAsXml() throws Exception { makePages("PageOne", "PageOne.ChildOne", "PageTwo"); submitRequest("root", "type:pages"); assertResponseIsXML(); assertResponseContains( " ); } public void testSymbolicLinksAreNotInXmlPageHierarchy() throws Exception { WikiPage page = makePage("PageOne"); makePages("PageOne.ChildOne", "PageTwo"); addLinkTo(page, "PageTwo", "SymPage"); submitRequest("root", "type:pages"); assertResponseIsXML(); assertResponseContains( " ); assertResponseDoesNotContain("SymPage"); } public void testGetDataAsXml() throws Exception { makePageWithContent("TestPageOne", "test page"); submitRequest("TestPageOne", "type:data"); assertResponseIsXML(); assertResponseContains("test page", " В структуре тестов очевидно воплощен паттерн ПОСТРОЕНИЕ-ОПЕРАЦИИ- ПРОВЕРКА 1 . Каждый тест четко делится на три части . Первая часть строит те- стовые данные, вторая часть выполняет операции с тестовыми данными, а третья часть проверяет, что операция привела к ожидаемым результатам . 1 http://tnesse .org/FitNesse .AcceptanceTestPatterns . 156 Чистые тесты 157 Обратите внимание: большая часть раздражающих мелочей исчезла . Тесты не делают ничего лишнего, и в них используются только действительно необходи- мые типы данных и функции . Любой программист, читающий эти тесты, очень быстро разберется в том, что они делают, не сбиваясь с пути и не увязнув в лишних подробностях . Предметно-ориентированный язык тестирования Тесты в листинге 9 .2 демонстрируют методику построения предметно-ориен- тированного языка для программирования тестов . Вместо вызова функций API, используемых программистами для манипуляций с системой, мы строим набор функций и служебных программ, использующих API; это упрощает написание и чтение тестов . Наши функции и служебные программы образуют специали- зированный API, то есть по сути — язык тестирования, который программисты используют для упрощения работы над тестами, а также чтобы помочь другим программистам, которые будут читать эти тесты позднее . Тестовый API не проектируется заранее; он развивается на базе многократной переработки тестового кода, перегруженного ненужными подробностями . По аналогии с тем, как я переработал листинг 9 .1 в листинг 9 .2, дисциплинированные разработчики перерабатывают свой тестовый код в более лаконичные и вырази- тельные формы . |