Pull to refresh

Откуда есть пошел xUnit

Reading time 6 min
Views 12K
Идея данной заметки — как гипотезы — появилась уже довольно давно, и все как-то не получалось… Но вот «на днях» (к моменту публикации — уже неделях) увидел подтверждение своего предположения что называется «из первых рук» (см. Kent Beck's answer to Unit Testing: Did the notion of using setup() and teardown() methods in test fixtures originate from JUnit?) и решил-таки воплотить эту задумку.

Речь здесь пойдет не о самом TDD, а, скорее, всего лишь о первых шагах в этом направлении. Но, думаю, знание истоков и понимание логики создателей является важный моментов в овладении тем, что выросло на засеянном ими поле… И в данном случае — тоже.

Итак… Давным-давно некий Кент по фамилии Бек со своим другом Вардом занимались программированием в среде Smalltalk. Я не знаю, какие именно задачи они решали — да это и не так важно, — но делали они это таким способом, которым и сегодня можно побаловаться (за очень редким исключением) только в средах этого семейства. Дело в том, что в Smalltalk-е нет абсолютно никакого разрыва между написанием программы и ее выполнением. И поэтому можно на ходу придумывать код, тут же его писать и выполнять. Более того, выполнять, как это ни странно может звучать, можно еще до написания… И это не сказка — могу показать, как это выглядит на практике.

Первый Smalltalk-овский инструмент, который нам понадобится, называется Workspace. По большому счету, это — примитивный (если не сказать сильнее) текстовый редактор. Единственное, чем Workspace выделяется в длинном ряду текстовых редакторов — это возможность выполнить написанное. (Похожее средство есть, например, в Есlipse, называется оно Display. Отличается, кроме всяких мелочей, в худшую сторону невозможностью выполнить код без запущенной программы, что, впрочем, является не виной этого инструмента, а, скорее, бедой всех систем с «криво-статической» типизацией.) Вот как Workspace выглядит в Smalltalk-е:

Workspace
Как видно в контекстном меню, можно просто выполнить строчку (или выделенный текст), можно напечатать результат, а можно его посмотреть в одном из двух доступных в данной Smalltalk-среде инспекторов и т.д.

«Все это замечательно, но как это связано с модульными тестами?» — весьма обоснованно спросит нетерпеливый читатель. Чтобы ответить, рассмотрим какую-нибудь простенькую задачу. Допустим, к примеру, мы хотим сделать из нашей Smalltalk-среды напарника для игры «Быки и коровы». Оставим пока в стороне излишества в виде специализированного графического интерфейса и попробуем сделать это максимально простым способом. Тот самый Workspace вполне подходит для этого: просим систему сначала создать объект игры, затем посылаем ему сообщения с нашим вариантом отгадки, а игра возвращает подсказку (количество быков и коров)… например, в виде точки: к примеру, 2 @ 3 (объекту 2 посылается сообщение @ с параметром 3 — в результате получаем экземпляр класса Point, где x = 2, y = 3) будет означать двух быков и три коровы; ответ 4 @ 0 означает, что ключ разгадан.

Общий план готов, приступаем к его воплощению. Разрабатывать можно так, как это делается обычно: создавая классы, в них методы и т.д. Но можно поступить по-другому — просто начать играть:


Мы ожидаем, что в ответ на это система создаст объект нужного нам класса. Чтобы в этом убедиться, мы можем проинспектировать полученный объект. Выбираем пункт меню Inspect It… и получаем предупреждение системы: она не знает, что следует понимать под именем BullsAndCows.

Missing Class Notification
Вообще, Smalltalk весьма доброжелателен по отношению к своему пользователю. Например, в данной ситуации это проявляется в том, что процесс компиляции кода (а именно на этом этапе мы сейчас остановились) при возникновении недопонимания (язык не поворачивается назвать это ошибкой) не заканчивается. Система лишь приостанавливает процесс, предлагая пользователю пути разрешения возникшей проблемы. В данном случае нас интересует создание нового класса («define new class»)


В предложенном «шаблоне» (который на самом деле является выражением на языке Smalltalk, обеспечивающим создание нового класса) желательно лишь указать категорию (пакет), в который будет помещен создаваемый класс — назовем его незатейливо: «BullsAndCows».


Жмем OK… и видим открывшееся окно инспектора с созданным экземпляром игры.


Мы получили желаемое. Следующий шаг: сообщаем игре наш вариант.

game guess: ???

Тут нам приходится задуматься: как лучше представить догадку? Скорее всего, так же, как и ключ… но мы и про ключ пока не вспоминали… У меня «автоматически» рождаются три варианта: 1) завести специальный класс для отгадки, либо 2) использовать число, либо 3) использовать строку. Чтобы выбрать, приходится задуматься… Я останавливаю свой выбор на строке, так как (забегая вперед) понимаю, что в дальнейшем мне придется сопоставлять ключ и отгадку, причем посимвольно, а строка и есть индексированная коллекция символов.


Выполнение второй строки приводит к возникновению ошибки.


По сообщению в заголовке окна видим, что экземпляр класса BullsAndCows просто не знает, как реагировать на полученное сообщение. Под заголовком представлены возможные варианты дальнейших действий: продолжить (проигнорировав ошибку), прервать, отладить или создать. Как видим, прерывание выполнения — далеко не единственная опция в данном случае. Продолжить можно, но это ни к чему не приведет — метод сам по себе не появится. Отлаживать тоже в данном случае смысла нет — здесь все понятно: нужно создать соответствующий метод.


Система интересуется, в каком классе создать данный метод (в самом BullsAndCows или где-то выше по иерархии?). Нам нужен первый вариант. Поскольку в Smalltalk-е набор методов объекта принято структурировать, относя методы к различным категориям, система предложит сделать и это.


Среди предложенных стандартных категорий мне приглянулась testing (проверка). Метод создается и открывается в отладчике для редактирования.


Заметим, что исключение не привело к «разматыванию» стека вызова (он показан в верхней части окна) и мы сможем продолжить выполнение программы, как только захотим. Но сначала давайте зададим реализацию для #guess:. Но для этого нам придется принять решение по вопросу, с которого, на самом деле, стоило начинать: что наша игра должна ответить в данном конкретном случае? Предлагаю сделать вид, что пользователь вообще ничего не угадал: вернем ему «0 быков и 0 коров». Реализуем максимально простым способом (Fake It):


Чтобы изменения в коде метода были откомпилированы, их нужно «принять» (Accept). После этого нажимаем кнопку Proceed, чтобы продолжить выполнение нашего сеанса игры… И получаем ожидаемый результат.


Надеюсь, здесь принцип уже стал понятен… На следующих итерациях, выдвигая все более строгие требования к поведению нашей системы, мы будем постепенно развивать ее: изменять (усложнять) созданные методы, при необходимости добавлять новые, вводить новые классы и т.д. Но каждый шаг будет начинаться с того, что мы продумываем правильное поведение разрабатываемой в данный момент части системы, выделяем его «признаки» — ожидаемые результаты — и те действия, которые необходимо выполнить для получения этих ожидаемых результатов. Далее просто записываем эти действия и начинаем выполнение полученной программы, не заботясь о наличии даже базовой инфраструктуры для ее работы. Любыми способами заставляем написанную программу работать так, как мы хотим. При необходимости улучшаем ее. Сигналом к завершению такой итерации служит совпадение реальных результатов с ожидаемыми и наша удовлетворенность полученным кодом.

До SUnit-а остался небольшой шажок: держать каждый раз «в голове» ожидаемый результат не выгодно — зря расходуются и без того ограниченные ресурсы человеческого интеллекта. Возникает естественное желание их куда-нибудь записать, из которого вытекает мысль, что записать можно прямо здесь же — в Workspace-е. Сюда же добавляются желание автоматизировать процесс сравнения полученных результатов с ожидаемыми, сохранить все уже проработанные варианты использования системы со всеми проверками и в дальнейшем их использовать для исключения регрессии… Требования к фреймворку практически готовы. Далее реализуем их наиболее простым способом — вот что пишет Kent Beck (см. ссылку на источник выше):

«Когда я приступил к проектированию первой версии xUnit, я использовало один из моих обычных приемчиков: превращать нечто в объекты; в данном случае весь Workspace превращался в класс. Каждый фрагмент тогда стал бы представляться методом (с префиксом „test“ в качестве примитивной аннотации).»

…Далее следует одно из важнейших в области разработки ПО открытий, которое к данному моменту уже лежит на поверхности: процесс формализации требований и получения по ним начального дизайна системы практически один к одному совпадает с процессом написания автоматических тестов. А это уже основа TDD: остается только обобщить полученный опыт, упорядочить практику написания управляющих тестов до реализации функционала, проанализировать свой опыт и убедиться, что есть несколько основных шаблонных приемов… и методология TDD в ее «классическом» виде готова.

***

Вместо заключения — немного критики. Мне полученная Беком и позже растиражированная повсюду архитектура не очень нравится. Вместо тестов-методов было бы удобно иметь тесты в виде отдельных объектов, необходимым образом связанных между собой и управляемых соответствующими инструментами IDE. Максимально естественным и удобным данный подход может быть в динамической, живой среде типа того же Smalltalk-а. …В общем-то, это уже тема для отдельных статей — с предварительным исследованием и разработкой. А исходным положением для них становится вывод о том, что широко используемый ныне xUnit и его клоны являются лишь первым приближением к решению задачи об использовании тестов для разработки программных систем — так что ли получается?
Tags:
Hubs:
+18
Comments 7
Comments Comments 7

Articles