В истории кода текущего проекта обнаружился переломный момент, когда с ним случилось нечто страшное интересное. Это сподвигло меня написать эту, и, наверное, несколько последующих статей, посвященных юнит-тестированию.
БОльшая половина кода нашего проекта на первый взгляд выглядит очень хардкорно. Если опустить пару моментов, где я вдоволь оттянулся с белой и черной шаблонной магией, то бросится в глаза, что остальная часть этой половины идет вразрез с заветами секты “ООП” и ее течения “многоэтажные иерархии классов” (да, я редко сажаю леса вроде приведенного на картинке ниже). В глаза бросится, да не долетит, а упадет и стечет (может быть даже прямо в ботинки).
При ближайшем рассмотрении код оказывается простым как пробка, понятным как дважды два и гибким, как резинка от трусов. Что-то нужно изменить? Нет проблем. Изволите перламутровые пуговицы вместо молнии? Пожалуйста. Что? Хотите декоративные пуговицы, а ширинку все же на молнии? Проще простого. И так далее. Причем для проведения хирургической операции по пересадки молнии не приходится укорачивать рукава и заодно перестраивать ещё и платяной шкаф, как это часто случается в индустрии программного обеспечения. Кстати, перекраска последнего по желанию заказчика (любой каприз за Ваши деньги!) тоже вовсе не изменяет цвет костюмов, в нем висящих. Код-как детали из детского конструктора, строй из них все, что хочешь.
Другая, хоть и меньшая, но не менее важная половина кода – полная противоположность всему вышесказанному. Вроде изящная структура интерфейсов и классов отлично видна даже в перевернутый бинокль (смотри картинку выше), но стоит посмотреть невооруженным глазом, как уши сами, опасаясь неминуемых последствий, начинают инстинктивно сворачиваться в трубочку. Местами этот спагетти-код настолько суров, что если потянуть за макаронину в своей тарелке, мясные тефтельки в тарелке у клиента воооон за тем столиком придут в движение и начнут водить хороводы. Если сравнить с более материальным миром, то попытка что-нибудь исправить в таком коде напоминает процесс штопания дырки в носке, который по непонятной причине неизменно заканчивается пришитым к ширинке рукавом майки (откуда у майки взялся рукав, лучше даже и не спрашивать, я давно забросил все попытки выяснить это). Причем после отпарывания рукава от ширинки обнаруживается, что до сих пор бывшими модельными ботинки каким-то образом превратились в ласты 47 размера, а стильные солнечные очки – в маску сварщика.
В общем, несмотря на кажущуюся структурированность, код отдельных компонентов имеет очень жуткую и совершенно неявную связность и практически не поддается модификации без применения тяжелой артиллерии класса “.. до основанья, а затем…”.
Самое замечательное во всей этой истории то, что разница между кодом, который лучше не трогать и кодом, с которым очень легко делать все, что угодно, очень уж очевидна. Порой кажется, что кто-то где-то провел границу в коде и сказал “с этого момента прекращаем гнать лажу по-старому и начинаем по-новому”.
Это мой первый проект за последние несколько лет, где практика юнит-тестирования и частично TTD были внедрены ровно на полпути. В моей практике это первый случай, где преимущества правильного использования юнит-тестирования не просто говорят сами за себя, а буквально кричат изо всех окон Студии, попутно постукивая шарахающихся зевак деревянной киянкой по голове.
Итак, большая половина кода в проекте была написана после внедрения технологии юнит-тестирования. Поначалу произведя эффект разорвавшейся клизмы, что всегда бывает в компаниях, где думают, что юнит-тесты – это исключительно средство тестирования кода (что есть неправда – это в первую очередь технология разработки), результат внедрения техники превзошел экономический эффект от использования технологии “сборка трезвым” на предприятии по производству яиц Фаберже с часовым механизмом. Не буду говорить о том, что подавляющее большинство найденных багов относится как раз к старой половине, при написании которой, похоже, руководствовались принципом “некогда фигней разной и тестами в частности заниматься, копать надо”, это мелочи. Тесты совсем не панацея от багов, хотя и помогают асимптотически устремить их число к нулю.
На самом деле, основной эффект от использования тестов проявился сейчас, спустя почти полгода после окончания разработки. Проект уже вышел и находится в стадии исправления и добавления фич. Ну, вы понимаете, стоит заказчику получить то, что он, собственно и заказывал, так начинается “а почему это работает так, а не эдак? Да, мы думали, что оно будет работать именно так, почему вы нам не сказали, ой, разве сказали, а когда, ой, в спеке написано, так мы ж его не читали, а почему вы нам не сказали, что его нужно читать, но все равно, сделайте нам ништяк, да так, чтобы нам ничего за это не было”. Так вот, если исправление или новая фича затрагивает новый код, то никаких проблем – из самого кода очевидно, что, где и как нужно изменить, а полтысячи разных тестов добавляют уверенности в том, что система не пойдет на йух после добавления новой фичи в ядро.
Совершенно другая история с кодом старым. К счастью, он достаточно прозрачен, и обычно не нужно производить археологических раскопок для поиска того места, которое нужно изменить. К несчастью, этот мир подчиняется закону Мерфи, поэтому нужно посмотреть внимательно на предыдущее предложение, уделив особое внимание словам “обычно не нужно”, понять, что к нашему случаю “обычно” и “не” не относятся и приготовиться к длительному туплению в монитор и потеряным человеко-годам в отладчике. В полном соответствии с этим законом, любые багфиксы или фичи приходятся на код, который непрозрачен настолько, что любые изменения в нем очень, наверное, похожи на разминирование минного поля вслепую. В целях обеспечения безопасности на производстве приходится одевать каску, залезать в окоп и оттуда уже запускать систему после любого изменения. Побочные эффекты постоянно вылезают феерверком там, где их ждали меньше всего. Кстати, как ни странно, тот факт, что половина кода все же покрыта тестами, позволяет отловить часть таких скрытых приколов.
Впрочем, это довольно типичная ситуация. Разработчики потратили время, кофе и пончики, разрабатывая и согласовывая интерфейсы и то, что называется “Top level architecture”, и придумали действительно очень красивую архитектуру. Но как только принялись собственно за кодирование, напрочь забыли о том, что собственно дизайн модулей, интерфейсы реализующих, тоже хорошо бы придумать и потом поддерживать. Дальнейшая разработка напоминает скорее постройку забора бойцами Советской Армии. Бери больше, кидай дальше, пока летит – отдыхай.
Когда разработка ведется с использованием юнит-тестирования, разработчик стремится написать тест для любой маленькой фигни. Это накладывает серьезнейший отпечаток на дизайн модуля, так как код в должен быть тестируемый в первую очередь. Значит, прежде чем бросаться грудью на амбразуры, програмиист должен подумать, а как, собственно, он собирается проверить, что этот кусок кода работает именно так, как задумано. А так как программист это существо, мозговой деятельности обученное, тут и начинаются интересные вещи. Первыми исчезают монстроидальные классы вида “универсальный всемогутор” с названием “менеджер чего-то там” и иже с ними, вместо этого появляется десяток-другой классов поменьше или вообще функций, каждая из которых решает какую-то отдельную небольшую задачу, не заморачиваясь о существовании других классов, функций и типов данных, ей неинтересных. Соответственно, твердое “нет” говорится процессам, где состояние объекта меняется неявно и незаметно. Функции, в которые раньше передавались огромные структуры для работы лишь с одним из их полей, начинают принимать только то самое поле. Внутренние и неявные связи исчезют на глазах. В следующий момент обнаруживается, что многие из мелких функций и классов теперь можно и нужно использовать в других местах проекта. Потом, при написании теста для отдельного класса обнаруживается, что для его удобного использования нужно применить Inversion of control, что и будет вскоре сделано, заодно решив еще целую кучу намечающихся проблем.
Короче говоря, цель юнит-тестирования – помочь программисту начать выступать не только и не столько в роли творца шедевра, но первым и самым главным пользователем и критиком своего же кода. “Плохой” код, то есть код, который трудно стыковать с другими модулями, очень сложно, а то и вообще невозможно тестировать, в то время как код, для которого написан и выполняется тест, гарантирует как минимум, что он стыкуется с другим относительно простым кодом – кодом теста – и его уже точно один раз (а может и больше) использовали и проверили в деле. Это помогает программисту увидеть слабости кода и изолировать потенциальные проблемы интеграции задолго до того, как потенциальные проблемы превратятся в проблемы реальные.

А у вас используется что-нибудь типа http://www.bullseye.com/ ?
Нед. Coverage tools на поверку не оказывают какого-нить существенного влияния.
Но оно оказывет влияние на мотивацию все оттестировать
А как у вас борятся с деятелями которые тесты не уважают? (я так понял у вас там тесты ввели на пол-пути по инициативе снизу)
А никак не борятся. Пущено на самотек. Иногда кто-то возникнет, что надо все тестит, да на том все и глохнет.
Проблема с code coverage в том, что на С++ обычно 100% покрытия не добиться. Например, код
Something * pPtrSomething = new Something(); if (pPtrSomething == 0) { // BLAH!!!! }Без применения святого бубна обеспечить покрытие кода в скобках будет почти невозможно. А это довольно простой случай, а что будет с кодос, который оптимизатор выкинет?
Ну хорошо, давайте добиваться не 100% покрытия, а 95%. А почему 95%, а не 80% и не 9.5%? Где та граница, после которой покрытие будет считаться хорошим?
Не знаю где граница, я думал позаимствовать полезную идею. Тут некоторые такое пишут. Я бы не против, но после последней смены курса я это переписываю, стараясь заставить работать. Как раз такой “универсальный всемогутор”.
На самом деле для себя я вывел эмпирическое правило. Если кто-то на стадии дизайна впихивает в диаграмму интерфейсов или, что еще хуже, начинает разработку с написания “class ManagerЧегоТоТам”, жди беды.
В code coverage тулзах ничего плохого нет, наоборот, они могу помочь программисту обнаружить места, которые он мог бы, да забыл протестировать. Но как метрика качества тестов оно, к сожалению, никуда не годится.