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

    Идея данной заметки — как гипотезы — появилась уже довольно давно, и все как-то не получалось… Но вот «на днях» (к моменту публикации — уже неделях) увидел подтверждение своего предположения что называется «из первых рук» (см. 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 и его клоны являются лишь первым приближением к решению задачи об использовании тестов для разработки программных систем — так что ли получается?
    • +18
    • 11,7k
    • 7
    Поделиться публикацией

    Похожие публикации

    Комментарии 7
      0
      Про тесты-объекты не очень понятно? Вы предлагаете создавать объект на каждой тестируемой фичи?

      Тогда объект будет состоять по сути из одного метода исполняющего тест… Идея выглядит не очень разумной. Объекты достаточно дорогостоящая структура. Или я что-то не понял?..

      за статью, спасибо… Весьма познавательно…
        0
        Про тесты-объекты не очень понятно? Вы предлагаете создавать объект на каждой тестируемой фичи?

        Тогда объект будет состоять по сути из одного метода исполняющего тест… Идея выглядит не очень разумной. Объекты достаточно дорогостоящая структура. Или я что-то не понял?..

        Именно это я и предлагаю.

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

        Примеров, где объект имеет «по сути» один метод, можно набрать много: например, блок (замыкание)… или нить…  Вообще, это признак хорошего объектного дизайна: один объект — одна ответственность.

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

        В общем, есть целый ряд задумок, связанных с TDD/BDD и дальнейшим развитием существующих принципов разработки с использованием тестов, где тесты-объекты очень нужны.
          0
          В чем заключается «дорогостоимость» объекта? В очередной раз отмечу: я ведь не с проста Smalltalk беру в примеры, а там (почти) все — объекты.

          Ну, если про Smalltalk говорить, то тесты-методы вполне себе объекты. Чем они вас не устраивают? )

          Мне понятна ваша идея. Но в ней есть внутренняя системная проблема… Тесты должны быть простыми до самоочевидности, иначе рано или поздно возникнет ситуация, когда придется тестировать тесты. И в конечном счете всё сведется к ситуации когда Тесты делаются ради тестов…

          А это основной аргумент тех, кто не испытывает пиетета TDD. Сам не раз наблюдал когда народ по полдня бодается с проблемами в очередной версии rspec'а вместо того, чтобы писать приложение. Последняя проблема с rspec'ами в Engines отняла конкретно несколько дней…

          Я уже честно говоря надоело подтрунивать над своими коллегами… Но у них аргумент… Rspec — требование заказчика… Хотя казалось бы какая разница заказчику?!
            0
            «Бодание» с проблемами используемой/-ого библиотеки/фреймворка вместо написания приложения — проблема, относящаяся к любому используемому в проекте стороннему (да и не только стороннему) коду. За все, в том числе и удобство, приходится платить. В данном случае за более развитые средства управления тестами, возможно, придется заплатить большей сложностью фреймворка.

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

              В смысле, «отдельно»? 0_о Код метода является частью объекта. И он в этом смысле хранится в самом объекте. В объектах, помимо методов, коду больше негде храниться…

              Вы предлагаете для каждого метода сделать свой отдельный объект. Собственно в RSpec'е, если я не ошибаюсь, так и есть. И проблема с Rspec'ом в RoR-приложениях, в частности в том, что запуск прогона в крупных проектах отнимает довольно немало времени.

              Ему, прежде чем начать тестирование, приходится проинициализировать все объекты, неговоря уже о том, что перед этим надо проинициализировать среду исполнения, то бишь приложение. И это порой может занимать десятки минут…

              Это я и имел ввиду, когда говорил, что объекты — дорогостоящая структура.

              Или я вас опять не понял…
                0
                В смысле, «отдельно»? 0_о Код метода является частью объекта. И он в этом смысле хранится в самом объекте. В объектах, помимо методов, коду больше негде храниться…

                Самое простое:
                Test newOn: [true should be: false].
                
                Test class >> newOn: aBlockClosure
                  ^ self new code: aBlockClosure
                
                Test >> code: aBlockClosure
                  code := aBlockClosure

                Вы предлагаете для каждого метода сделать свой отдельный объект. Собственно в RSpec'е, если я не ошибаюсь, так и есть. И проблема с Rspec'ом в RoR-приложениях, в частности в том, что запуск прогона в крупных проектах отнимает довольно немало времени.

                Ему, прежде чем начать тестирование, приходится проинициализировать все объекты, неговоря уже о том, что перед этим надо проинициализировать среду исполнения, то бишь приложение. И это порой может занимать десятки минут…

                Это я и имел ввиду, когда говорил, что объекты — дорогостоящая структура.

                Едва ли задержки вызваны самим фактом хранения тестов в отдельных объектах (если это действительно так реализовано в RSpec). По крайней мере, я не могу навскидку нафантазировать каких-либо возможных причин для этого. В крупных проектах и на стандартном xUnit прогон тестов отнимает немало времени — там тоже нужно проинициализировать все объекты и среду. Возможно, в RSpec время тратится на что-то еще, может быть, в тестах используется DSL, который транслируется в момент выполнения? Если у вас есть конкретная информация об эффекте — будет интересно узнать.
        0
        Скажу по секрету, что Display в Eclipse перекочевал из VisualAge Smalltalk. Впрочем, как и SWT — это порт CommonWidgets/CommonGraphics из того же VisualAge Smalltalk.

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

        Самое читаемое