The Art of Unit Testing



    Есть некоторые категории знаний, которые профессиональный разработчик познает в процессе своей работы, не прилагая к этому особенных дополнительных усилий. Вот, например, мало кто из нас читал замечательную книгу по регулярным выражениям Джеффри Фирддла, чтобы познакомиться с одноименной темой. Безусловно, есть масса людей, для которых «регвыры» стали смыслом жизни и без подобных фундаментальных знаний никак не обойтись. Но в большинстве случаев пары мелких статей и справки в соответствующем разделе документации будет достаточно для более или менее комфортной работы с регулярными выражениями (если такое понятие, как «комфортная работа» с регулярными выражениями вообще существуетJ).

    Аналогичным образом мы обычно относимся и к изучению юнит тестирования. Ведь юнит-тесты – это же не rocket science; для их изучения не требуется многолетняя подготовка и множество бессонных ночей проведенных за изучением толстенных «талмудов» от гуру юнит-тестирования. Концепцию автоматизированного тестирования кода можно объяснить за 10 минут, а познакомившись с одним из тестовых фреймворков семейства xUnit (еще 15 минут), вы сможете работать с любым другим фреймворком практически сразу же. Затем нужно будет потратить еще 20 минут на изучение какого-нибудь изоляционного фреймворка, типа Rhino Mocks, и, вуаля, у нас есть еще один профессионал в области юнит-тестов.


    Подобный уровень владения инструментом, подкрепленный опытом, прагматизмом и здравым смыслом, достаточен для комфортной работы, поэтому может сложиться впечатление, что опытному разработчику пользы от этой книги не будет никакой. Я, честно говоря, был такого же мнения и вряд ли заинтересовался бы этой книгой, если бы мне не пришлось готовить доклад по юнит-тестированию для наших американских коллег. Ведь одно дело — это владение вопросом для собственного использования, другое дело – это внедрение чего-то нового и изменения мировоззрения команды разработчиков, большая часть из которой является твоими боссами.

    Забегая вперед, стоит сказать, что я заблуждался икнига The Art of Unit Testing by Roy Osherove будет полезна даже опытным разработчикам, но давайте обо всем по порядку.

    Структура книги такова, что автор идет от наиболее простых и базовых понятий к более продвинутым вещам. Начинается все с простых примеров и определений, дается введение в TDD, рассказывается о «швах» приложения и об использовании «фейков», таких как моки и стабы, а также кратко описываются разные изоляционные фреймворки.

    С самого начала Рой пытается вдолбить в голову читателя одну важную мысль: качественные тесты не менее важны, чем качественный production код! О том, что такое хорошие тесты, посвящена целая глава 7 The pillars of good tests, но даже за ее пределами тень «хороших» тестов будет преследовать читателя практически постоянно. О качестве кода написаны, наверное, десятки книг и бесчисленное множество статей; все мы знаем о том, как важно писать простой в сопровождении код и что делать для повышения его качества. Но к качеству тестов наше отношение зачастую не столь осмысленное, хотя плохие тесты могут испортить вам жизнь ничуть не хуже го$#о-кода в бизнес-логике.

    Нет никакого смысла в написании плохих юнит тестов, если только вы не новичок в этом деле и не учитесь таким образом писать хорошие тесты. Если вы по неосмотрительности собираетесь писать плохие тесты, то с тем же успехом вы можете вообще отказаться от этого занятия. Это убережет вас от неприятностей, связанных с отставанием от графика и избавит вас от дополнительных проблем с сопровождением.

    Очень здорово, что помимо принципов юнит тестирования Рой уделяет внимание и таким важным аспектам, как внедрение юнит тестирования в организации, тестирование унаследованного (legacy) кода и влиянию тестов на дизайн приложения. В вопросах дизайна Рой считает, что testable дизайн обусловлен, прежде всего, ограничениями языка или среды программирования и не существует в динамических языках программирования (поскольку там мы «замокать» можем все, что угодно). Я с таким подходом не согласен, поскольку считаю, что хороший дизайн по умолчанию хорошо тестируем, и что возможность написания юнит тестов является хорошим признаком простоты контрактов классов и признаком того, что класс не берет на себя слишком много.

    ПРИМЕЧАНИЕ
    Подробнее об использовании юнит тестов как «лакмусовой бумажки» плохого дизайна можно почитать в заметке “Идеальная архитектура”
    .

    По признанию самого Роя он потратил на написание книги 3 года — и это больше, чем он потратил на «создание» двух своих детейJ и периодически это чувствуется. Так, например, в разных местах книги используются разный формат диаграмм классов, иногда отличается наименование тестовых классов и методов, да и вообще, ощущение дежавю периодически посещает. Есть и другие мелкие замечания. Так, автор на протяжении двух сотен страниц использует стандартный синтаксис утверждений библиотеки NUnit, а с 200-й страницы, вдруг начинает использовать Assert.That и заявляет о том, что этот синтаксис более декларативен. Очевидно, что года через полтора после начала работы над книгой Рой добрался до этого синтаксиса, который ему понравился, а сил и времени на изменение предыдущих примеров уже не было.

    Единственным существенным замечанием к этой книге является слабое раскрытие темы параметризованных юнит тестов. Да, Рой упоминает вскользь об этой возможности, но уж очень поверхностно и где-то ближе к концу книги. Чувствуется некоторая несправедливость, когда о том, что плохо рассчитывать на порядок запуска юнит тестов автор тратит 4 (!) страницы, а на параметризованные тесты, которые могут сэкономить массу времени и существенно повысить читабельность тестов, тратится от силы 2 абзаца.

    Можно найти и другие моменты, в которых ваше мнение будет отличаться от мнения автора, но в целом, книга “The Art of Unit Testing” является одним из лучших источников информации по теме юнит тестирования, которая может либо изменить ваше мировоззрение в правильную сторону, либо обобщить и структурировать уже существующие знания.

    Оценка: 4+

    Дополнительные ссылки по теме

    Книги

    The Art of Unit Testing by Roy Osherove
    xUnit Test Patterns: Refactoring Test Code by Gerard Meszaros
    Working Effectively with Legacy Code by Michael Feathers
    Pragmatic Unit Testing in C# with NUnit, 2nd Edition by Andy Hunt and Dave Thomas

    Подкасты

    The History of JUnit and the Future of Testing with Kent Beck
    Kent Beck, Developer Testing
    Метки:
    Поделиться публикацией
    Похожие публикации
    Комментарии 19
    • 0
      Прочитал, неплохо. Всем советую, причем знание и использование в работе .net не необходимо.
      • 0
        Спасибо за совет, а то ближе к концу поста стал задумываться не слишком ли книга C#/.NET специфична и пригодятся ли «паттерны» из неё в PHPUnit.
      • 0
        >В вопросах дизайна Рой считает, что testable дизайн обусловлен, прежде всего, ограничениями языка или среды программирования и не существует в динамических языках программирования (поскольку там мы «замокать» можем все, что угодно). Я с таким подходом не согласен, поскольку считаю, что хороший дизайн по умолчанию хорошо тестируем, и что возможность написания юнит тестов является хорошим признаком простоты контрактов классов и признаком того, что класс не берет на себя слишком много.

        В чём-то он прав, в чём-то вы. В PHP (уж простите, но это единственный язык для которого и на котором я пишу тесты) мы можем замокать/застабить почти что угодно (по крайней мере пока дело идёт об ООП и/или callable, а если нет, то почти почти всегда можем подвергнуть код безопасному рефакторингу без тестов и свести его к ООП/callable), а благодаря PHPUnit (и, конечно, его автору Sebastian Bergmann) это делается относительно легко, без самостоятельного копания с отражениями и прочей «магией». В этом прав он.

        Но такие тесты, как правило, перестают выполнять одну из своих функций — служить документацией к коду. Они по прежнему показывают удачный был рефакторинг реализации интерфейса (в широком смысле слова, вроде слово «контракт» в .NET для этого используется) или нет, но что тест проверяет, что делает проверяемый код в данном кейсе уже не понять. При необходимости изменения интерфейса («контракта») такого «юнита» приходится разбираться не только с кодом реализации, но и с кодом тестов (особенно в PHP, где DSL являются таковыми лишь условно), причём легко допустить ошибку и непосредственно в тестах (которая вполне может перетечь в реализацию). Вроде это называется «хрупкими тестами». Избежать этого может помочь легкотестируемая архитектура. Не просто тестируемая (в динамических языках она (почти?) всегда тестируемая, а именно легкотестируемая. В этом правы вы. Правда, увы, легкая тестируемость не является достаточным признаком хорошей архитектуры :(
        • 0
          P.S. В блоге «Тестирование» или «TDD» топик бы лучше смотрелся, имхо.
          • 0
            «мы можем замокать/застабить почти что угодно»

            Есть существенное ограничение на статические методы. Впринципе, его можно обойти, если гонять тесты в разных процессах, но в том же PHPUnit оно присутствует.

            Кстати, вот хотел спросить о вашем опыте в PHPUnit. Насколько оправдано использование рефлексии для проверки или подстановки разных значений? Вот не могу понять, это хорошая практика, или плохая. С одной стороны, мы с помощью рефлексии можем подставить и подсмотреть нужные значения в класс не дергая его другие методы, и значит, тестировать только один метод. С другой — мы таким образом можем тестировать «коня в вакууме», метод работает, но совершенно не с тем API, с которым используется в коде.
            • 0
              И статические методы мокаются уже давно. sebastian-bergmann.de/archives/883-Stubbing-and-Mocking-Static-Methods.html — пост про это от создателя PHPUnit (и вообще интересный блог :) ) Или вы какой-то нюанс имели в виду?

              Имхо, плохая. Во-первых, нарушение инкапсуляции — тест знает, что внутри объекта, во-вторых, это тестирование состояния, а от методов нужно поведение. Простой пример — тестируем примитивные акцессоры приватного свойства. Записываем с помощью рефлексии значение, получаем геттером — ок. Запускаем сеттер, читаем через рефлексию (напрямую или ассертами PHPUnit) — ок. Но могли бы эти два теста объединить в один — записываем сеттером, читаем геттером — результат тот же, ок, тест проверяет именно то, что нам нужно (а нужно нам не то что переменная устанавливается или возвращается, а что установленную сеттером можно прочитать геттером), без всяких тормознутых рефлексий (отражение — мне всё же больше нравится :) ). Но один тест против двух — это мелочи по сравнению с тем, что нас ждёт если мы решим, например, перенести эту переменную (вместе с другими) в другой объект, агреггированный тестируемым. Нам придётся в первом случае переписывать оба теста, делать «рекурсивное» отражение и т. п. А если решим её хранить в базе или облаке? А простой тест из вызова сеттера и геттера будет продолжать работать в любом случае (если не ошиблись) и показывать, что с сохранением и получением свойства у нас всё ок.

              Недостаток отражений в тестах — сильная связанность тестов с реализацией, а не с интерфейсом/контрактом, актуально только если клиенты тестируемого юнита также с реализацией сильно связаны, чего в хорошей архитектуре быть не должно, но, увы, встречается. Тогда тестами с отражениями (а точнее тестами состояния) мы, если есть желание и ресурсы на улучшение архитектуры, фиксируем нежелательные связи с клиентами тестами состояния, вводим в них «шов» в виде, например, геттера для пока публичного свойства, прогоняем тесты, если всё ок (ввели швы без ошибок), делаем свойство приватным, опять прогоняем тесты, если всё ок, то изменяем тесты состояния (установку публичного свойства для клиентов) на тест поведения (вместо геттера подставляем стаб) и получаем из интеграционных тестов клиентов и нашего юнита, обычный юнит-тест клиента. Что не мешает нам и написать (вернее «скопипастить») и нормальный интеграционный тест, но уже на поведение, а не на состояние.

              P.S. Блин, и зачем я слепой печатью овладел, поток сознания на руках не спотыкается почти :(

              • 0
                Спасибо за блог. А то в документации это изменение пока не освещено.

                Но ограничение как было, так и осталось:

                «This approach only works for the stubbing and mocking of static method calls where caller and callee are in the same class.»

                Хотя впринципе не понимаю, почему тупо нельзя взять код статического класса, наставить туда маячков и затем вгрузить вместо основного.

                Да, я так подумал, наверное вы правы. Рефлексия для проверки значений это плохая идея. Но допустим, в PHP всё ещё практично доступаться к объектам не через методы (кои мы можем застабить), а через свойства. А переопределять значение свойства можно рефлексией.

                Вот ещё один пример придумал. Ксть класс, его конструктор берет id, по нему из базы получает данные. Есть метод, он проводит с данными манипуляции и дает что-то на выход. Мы же не будем делать стабы для функции доступа к БД. Скорее всего мы подменим конструктор и заполним свойства требуемыми значениями через рефлексию…

                Вот в такой ситуации не понимаю как лучше. Тут два варианта, или подменять внутренние свойства, или делать стаб, который к тестируемому коду вообще отношения не имеет. А вдруг там в конструкторе что-то поменяется и данные он будет тянуть не с БД, а с АПИ. И потом тест упадет, из-за неправильных стабов. То есть тут как ни крути, а они «хрупкие». И даже если соблюдать правильность соглашений при вызове метода, хрупкость получится из-за стабов.
                • 0
                  Понятно, как-то не приходилось со статикой сталкиваться, а в детали не вникал, видел что возможность есть. В принципе можно обойти созданием фейкового класса и ручками его инклудить в файл с тестами, чтобы автолоад не сработал.

                  Да, можно и дешевле получается. Но публичные свойства доступны и без отражений, единственное что может понадобиться, так ввести какой-нибудь IoC, если его ещё нет, чтоб в тестах просто писать
                  $o=new Class();
                  $o->p = 'SmthOld';
                  call_smth($o);
                  $this->assertEqual('SmthNew', $o->p);
                  Отражения понадобятся только для тестирования поведения наследников классов с приватными свойствами, когда нормальных акцессоров к ним нет.

                  >Мы же не будем делать стабы для функции доступа к БД.

                  Первое что я делаю, когда попадается «спагетти» — это заменяю вызовы mysql_query() на $mysql->query(), где $mysql — экземпляр примитивного класса-обёртки для mysql_*. Если используется PDO или mysqli, то вытаскиваю их создание из кода, который надо покрыть тестами всеми правдами и неправдами, вплоть до глобальных переменных, лишь бы обеспечить возможность мокнуть/стабнуть вызовы к БД. То же относится к вызовам типа curl_* или файловым, хотя в PHPUnit и есть развитые средства работы с БД и ФС, но их я оставляю для функциональных тестов, когда проверяется почти вся система в сборе (до приемочных с Selenium не доходил). Ну а как только тесты готовы с моками/стабами для БД/ФС, то убираю детали хранения из кода вообще, реализуя что-то вроде репозитория, перенося в него все вызовы функций (вернее к тому времени уже методов) низкоуровневых API, оставляя исходный тест в качестве интеграционного, и «копипащу» его как юнит, заменяя моки обращения к БД на моки обращения к репозиторию, куда более меньшие, простые и с нормальными (почти всегда) именами. Если понадобится сменить хранилище, то нужно будет только сменить реализацию репозитория, протестировать только её, а тесты исходного кода от нового хранилища зависеть не будут.
                  • 0
                    Опять таки, спасибо за развернутый ответ. Как минимум, кое-чего полезного почерпнул.
          • 0
            Книга хорошая. Является классикой для .Net (хотя наверное лучше написать «является самой популярной для .Net»).
            • 0
              Даже по названию видно, что там лишь примеры на .NE, а самом искусство универсально :)
            • +3
              Денис Гладких писал на хабре об этой книге больше года назад habrahabr.ru/blogs/net/107678/, пусть ссылка будет здесь :)
              • +4
                Книга действительно отличная, очень помогла мне разобраться в тестировании. Я ее даже грешным делом перевел на великий и могучий с разрешения самого Роя. К сожалению, пока российские издательства не заинтересовались вопросом публикации.
                • +1
                  Начал читать, тоже возникла мысль перевести. В паблик выложить не вариант?
                  • 0
                    Вариант, конечно, но авторские права же и всё такое.
                    • 0
                      Если б авторские права не уважал, то так и написал бы «в паблик выложите, раз никто не хочет печатать: издательства — ССЗБ. Или в „личку“ мне скиньте :)»
                      • 0
                        Может быть, вы Рою напишите и спросите разрешения? А он может у своего издателя узнать.

                        Недавно вышло печатное издание перевода книги «Типы в языках программирования». А до этого переводчики выложили свой труд в свободный доступ для скачивания.
                  • +2
                    Книга безусловно отличная, но тех, кто давно использует Unit-тесты в своих проектах, стоит предупредить, что многое из книги покажется Вам очевидным. Я прочитал ее с удовольствием, отнесся к ней как к систематизации всех своих разрозненных знаний/ощущений по Unit-тестированию.
                    • 0
                      Господа, я так понял, в сабжевой книге не особо освещены всякие узкие места, раз их в камментах начали обсуждать. Сколько ни читал статей по тестированию, во всех примеры вроде assert 1!=2 //wow оно тестируется!!!111. А дальше то что? Показано, как потратить время впустую. А вот как выбрать стратегии тестирования; сколько чего и как стоит покрывать тестами; как вообще писать код, чтобы он легко тестировался, — этого нигде не нашел. Посоветуйте чтнть пожалуйста.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.