Pull to refresh

Comments 19

Прочитал, неплохо. Всем советую, причем знание и использование в работе .net не необходимо.
Спасибо за совет, а то ближе к концу поста стал задумываться не слишком ли книга C#/.NET специфична и пригодятся ли «паттерны» из неё в PHPUnit.
>В вопросах дизайна Рой считает, что testable дизайн обусловлен, прежде всего, ограничениями языка или среды программирования и не существует в динамических языках программирования (поскольку там мы «замокать» можем все, что угодно). Я с таким подходом не согласен, поскольку считаю, что хороший дизайн по умолчанию хорошо тестируем, и что возможность написания юнит тестов является хорошим признаком простоты контрактов классов и признаком того, что класс не берет на себя слишком много.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Да, можно и дешевле получается. Но публичные свойства доступны и без отражений, единственное что может понадобиться, так ввести какой-нибудь 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, оставляя исходный тест в качестве интеграционного, и «копипащу» его как юнит, заменяя моки обращения к БД на моки обращения к репозиторию, куда более меньшие, простые и с нормальными (почти всегда) именами. Если понадобится сменить хранилище, то нужно будет только сменить реализацию репозитория, протестировать только её, а тесты исходного кода от нового хранилища зависеть не будут.
Опять таки, спасибо за развернутый ответ. Как минимум, кое-чего полезного почерпнул.
Книга хорошая. Является классикой для .Net (хотя наверное лучше написать «является самой популярной для .Net»).
Даже по названию видно, что там лишь примеры на .NE, а самом искусство универсально :)
Книга действительно отличная, очень помогла мне разобраться в тестировании. Я ее даже грешным делом перевел на великий и могучий с разрешения самого Роя. К сожалению, пока российские издательства не заинтересовались вопросом публикации.
Начал читать, тоже возникла мысль перевести. В паблик выложить не вариант?
Вариант, конечно, но авторские права же и всё такое.
Если б авторские права не уважал, то так и написал бы «в паблик выложите, раз никто не хочет печатать: издательства — ССЗБ. Или в „личку“ мне скиньте :)»
Может быть, вы Рою напишите и спросите разрешения? А он может у своего издателя узнать.

Недавно вышло печатное издание перевода книги «Типы в языках программирования». А до этого переводчики выложили свой труд в свободный доступ для скачивания.
Книга безусловно отличная, но тех, кто давно использует Unit-тесты в своих проектах, стоит предупредить, что многое из книги покажется Вам очевидным. Я прочитал ее с удовольствием, отнесся к ней как к систематизации всех своих разрозненных знаний/ощущений по Unit-тестированию.
UFO just landed and posted this here
Sign up to leave a comment.

Articles