Если нельзя, но очень хочется, то нужно обязательно и ничего в мире не стоит того, чтобы делать из этого проблему!


Интересна Java? Кликай по ссылке и изучай!
Если тебе полезно что-то из того, чем я делюсь в своем блоге - можешь поделиться своими деньгами со мной.
с пожеланием
столько времени читатели провели на блоге - 
сейчас онлайн - 

суббота, 19 января 2013 г.

#9 Пишем Web проект на Java в Eclipse для Tomcat. Билдим Ant. Проверяем Hudson. Тестим jUnit + EasyMock + jWebUnit. Коммитим в Svn.

Ссылка на прошлую серию... Подошло время вынести описание экзамена из класса в отдельный xml файл. Для этих целей я хочу создать новую реализацию ExamQuestionAnswer которой на вход будет передаваться название экзамена. Внутри она будет подгружать xml файл и вести себя как обычный мультивопросный экзамен. Правильные ответы будут так же храниться в xml, но читать их будет уже экзаменатор.

Для работы с xml возьмем библиотеку JDom. Выкачать ее можно на официальном сайте.



Скачанную библиотеку стоит распаковать в папку lib проекта


А потом подключить ее в IDE



Я не хочу повторять прошлых ошибок при тестировании модулей работающих с файлами. Раньше ж как было. Модуль с помощью каких-то средств java читал/записывал данные из/в файл. Тест, перед тем как дернуть метод модуля, готовил исходный файла после читал значение файла - для доступа к файлу использовался тот же метод, что и в реализации модуля. Купа ненужной работы как по мне.

Что, если теперь тестировать модуль не в связке с парсером, а отдельно от него, а функцию парсера эмулировать с помощью моков? JDom не первый год пишется - думаю ребята его основательно потестили. Раз так, мне надо отделить интерфейсом загрузку данных в модуль от самого тестируемого модуля, а потом и замокать новый интерфейс.

Так же я хочу отныне вести разработку исключительно через Test Driven Development - мы его неоднократно использовали, когда писали требование впереди кода, его реализующего. Теперь так будет всегда. Походу я расскажу ряд специфических моментов, позволяющих избавиться от всех тех глюков, с которыми мы боролись раньше, таких как Debug, к примеру. Ну что, начнем?

Если взглянуть на реализацию мультивопросного экзамена, то его можно разделить на две части: логика работы самого класса и загрузка в него данных (помечено красным)


Список QestionAnswers как раз и есть тот интерфейс, который разделяет логику работы с экзаменом от логики загрузки вопросов. Ах да! В этот интерфейс еще стоит включить еще и название экзамена, а так же список правильных ответов. Вот и нарисовался новый интерфейс.

Попробуем реализовать первое требование, которое отобразит наши мысли в коде. Замечу так же, что все требования отныне будут завязаны исключительно на интерфейсы - так мы сможем тестировать независимо все реализации интерфейсов.

Создадим абстрактный тест ExamQuestionAnswerTest.


Почему абстрактный? Потому что в нем будет сосредоточена логика тестирования интерфейса ExamQuestionAnswer, а все наследники этого абстрактного набора тестов будут конкретизировать какую именно реализацию мы собираемся тестировать. Сейчас все станет понятно.

Создадим первый тест, проверяющий, что экзамен сохраняет имя, которым его назвали. Так же создадим абстрактный метод, получающий экземпляр класса с установленным именем.


Если попытаться запустить этот класс как jUnit тест, то напоремся на сообщение что класс не содержит тестов.


Но мы ведь явно аннотировали один метод как Test. Но класс ведь абстрактный! Ничего страшного, как только мы реализуем наследника этого абстрактного набора тестов, мы сможем его запустить как полноценный набор тестов. Все свои тесты наследник получит от исходного абстрактного родителя.

Создадим новый класс наследник.


IDE автоматически предложит нам реализацию с пустышкой методом getInstance


его мы и наполним смыслом.


а потом запустим как тест


Тест провалился по двум причинам - если изучить детальнее stack trace то можно увидеть, что ожидалось "first test" а получили какой-то класс. Дето в том, что я ошибся в тесте и сравнил строку с экземпляром класса. Исправим эту неточность.


Вторая же бяка состоит в том, что в наследнике я воспользовался конструктором не предназначенным для передачи в него имени экзамена. Конструктор используется для создания экзамена для пользователя, а экзамен прошит в самом геттере.


Выход есть - создадим новый конструктор и поле для сохранения имени экзамена.


и воспользуемся ним в самом тесте


Запустим тест, и о чудо!


Конечно же мы поломали интерфейс модуля - вся система думает, что конструктор с одним строковым параметром - это конструктор принимающий имя файла, а нет - теперь это конструктор принимающий название экзамена. Так же раньше система думала, что достаточно вызвать конструктор класса не заботясь об названии экзамена, но теперь судя по всему клиентам этого класс придется указывать названи экзамена. Стоит исправить эти неточности.

Я предлагаю ввести уже знакомую нам фабрику. Все клиенты класса экзамена буду пользоваться ею для получения экзамена. Для начала создадим пустой final класс



А потом воспользуемся автоматическим рефакторингом Introduce Factory. Если теперь просмотреть на изменения, предлагаемые IDE с этим рефакторингом, то можно заметить что это не совсем то, что я хотел, но все же спасибо - от большей части работы IDE меня избавило.





Конструктор мы не даром попросили сделать не публичным. Более того я весь класс сделаю не публичным. Отныне про эту реализацию знает только фабрика и больше никто!


Посмотрим теперь на фабрику - ее надо слегка подкорректировать, убрав информацию о том, какую реализацию она продуцирует. Фабрика должна выпускать наружу только интерфейс.


Сделаем Inline консруктору, который остался публичным


Не может? А мы хорошо попросим


Все равно не может?


Ладно - сделеаем эту руками, тупо удалив конструктор а его тело скопировав в другое место


После этого телодвижения стало видно кто является клиентом этого класса.


Мы научим этого клиента жить по новым правилам.


Но так как Factory немного не соответствует, мы ее исправим.


Вроде как все - запустим все тесты


Этот танец можно считать завершенным. Так как все тесты зелененькие и я ничего не поломал, а сделал немало, то я могу сохранится, отправив код в реппозиторий. Теперь я буду часто коммититься - как можно чаще - как только все тесты стали зелеными после какого-то моего телодвижения. Так безопаснее. Коммитить jdom библиотеку я пока не буду - поспешил я с ней - до парсинга xml еще пару коммитов точно.


Сделали мы не много, но и не мало - мы начали разделять сферу ответственности между классом реализаций экзамена и ее фабрикой.

Пошли дальше - чтобы очистить класс окончательно от конкретики, нам надо перенести в фабрику все захардкодженные (так говорят, когда в классе прошиты какие-то константы) вопросы и ответы, а так же метод для удобного добавления их.

рисунок 38

Вначале я сделаю Inline вспомогательного метода add.



После я создам новый класс QuestionAnswer


и перенесу туда содержимое внутреннего класса QuestionAnswer, сделаю его публичным, а поля его видимым в пределе пакета.


После я сделаю Inroduce Parameter для поля questionAnswer


в результате чего чуть позже смогу передать в исходный класс параметром список questionAnswers


Теперь я спокойно могу перенести инициализацию вопросов/ответов с класса экзамена в фабрику.


а затем попросить IDE создть мне недостающую локальную переменную.


IDE конечно немного сглючит, но я его поправлю


Запускаем все тесты.


Зеленые? Сохраняемся...



Ну что? Наш экзамен теперь стал очень простым в реализации


В его ответственность входит последовательный вывод вопросов и ответов и запись вариантов ответа пользователя. Ну так потестим это!


Упс, у нас нету метода - создадим его


Как ты мог догадаться - его стоит сделать абстрактным и реализовать в наследнике.



Наполним его смыслом.


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

Думаю красивее будет все же переместить тесты с пакета test.exam в пакет exam. Но если я это сделаю, то SVN сума сойдет - он подумает что я удалил старые файлы и создал новые. Потеряю всю историю коммитов. А что если в свойствах проекта указать, что исходники находятся в другой папке: не src а src/test?

Идем в свойства проекта


и изменяем текущую конфигурацию исключая папку src/test из общего набора исходников








а потом добавляя папку src/test как новый набор исходников





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


Чтобы компиляция удалась - надо всего лишь изменить во всех тестах пакет.


Но оптимальнее это сделать через глобальный поиск/замену




То же предлагаю сделать и с импортами



После этого все снова будет компилироваться


За исключением нашего рабочего набора тестов. Его стоит немного доработать и конвертировать Array в List.


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


Тут я ошибся понадеявшись на toString метод. С массивами в java все немного не так как с остальными объектами. Исправляется довольно просто.


Зеленые тесты? Сохраняемся.


Мне пришлось сохранить так же добавленную библиотеку JDom, потому как с этим коммитом в репозиторий уходит файл classpath а там есть упоминание про библиотеку.



Фух! Что там у нас дальше? Что еще можно потестить в экзамене?

К примеру то, что имя студента сохраняется в результатах. Тут, как и в прошлые разы недостающий метод просим создать IDE


дорабатываем то, что не доделал IDE


и реализовываем в наследнике


после наполняем смыслом пустышку-метод


и запускаем все тесты! Почему мы запускаем все тесты? Да потому что так мы не упустим ничего лишнего (я сейчас говорю про новую багу, добавленную с внесенными изменениями) с каждым нашим нововедением. Правило простое - что-то сделали? Надо запустить тесты. Если код не компилится, то надо его как можно быстрее сделать компилируемым и сразу после запустить все тесты. Трудность возникает тогда, когда "все тесты" запускаются долго (больше минуты), тогда у программиста отпадает желание запускать их часто - он попросту простаивает. Как вариант можно начать писать мануалы как я, и пока запускаются тесты - чиркнуть что-то в блоге. Но скорее всего так делают не все, а потому выход очевиден - надо стремиться к тому, чтобы тесты выполнялись молниеносно - меньше секунды. Это сильное ограничение на тесты исключает все интеграционные тесты из арсенала разработчика просто потому, что они работают долго. Наши к примеру поднимают jetty server а это пар секунд занимает. Это еще один недостаток функциональных тестов. Так что долой функциональные тесты - вперед юнит тесты!


Зелененькое? Как думаешь что будет сейчас? Save :)

Я больше всего люблю сохраняться играх, Word и SVN. В играх после сохранения не страшно, что тебя убьет очередной монстр внезапно появившийся из за угла, в Word роль этого монстра выполняет электрик внезапно полезший в электро-щиток в нашем доме, а в разработке этим монстром чаще всего бывает такой глюк: вот уже 18:00 - пора домой, но я вот вот доделаю свой таск... потом проходит еще пол часа и я понимаю, что заблудился в собственном коде и не знаю что делать дальше - откатиться назад и потерять пол дня работы не вариант - я озлобленный и обессиленный продолжаю решать нерешаемую архитектурную задачу, не желая принимать во внимание тот факт, что где-то меньше часу назад мной было выбрано неправильное архитектурное решение - даже если я пойму где ошибся, вернуться назад я могу только па пол дня назад. Так и сижу до 21:00. Знакомо? Кто виноват? Монстр? А мне кажется не вовремя нажатая кнопка Save.



Что дальше? Есть какое либо поведение которое можно потетсить в экзамене?

Мне лично кажется большим и не атомарным вот этот тест.


Одним из свойств тестов должна быть атомарность - то есть он должен быть таким, чтобы его нельзя было подробить на меньше тестики. А этот можно!

Зачем это делается? Чтобы один слетевший тест говорил об одной проблеме в коде, а не сразу о нескольких. Представим, что этот тест слетит на первом assert. Мы обратим внимание на тест и справим ошибку в коде - запустим снова все тесты и есть отличная от нуля вероятность, что слетит этот же тест, но на assert расположенном несколько ниже. Это начинает бесить после 3й итерации. По этой причине рекомендуется, чтобы один тест проверял что-то одно и содержал один assert, либо группу assert проверяющих один аспект поведения кода.

Фас!


Напомню: все синенькое - это те участки, которые раньше были в коде, но поменяли свое местоположение а красное - новое добавленное мной.

Стоит так же заметить, что в ходе разделения у меня появилась еще одна идея для очередного теста, но так как все тесты зеленые - я наверное сохраняюсь. Иначе может прийти монстр!


Save!



А теперь реализуем новую функциональность


Требование не прошло, а значит время править код.

Простая проверка должна помочь.


Таки помогла.

Все тесты, и если зеленое - save. Может на это уже макрос пора написать?


Save

Не знаю как тебя, но меня уже достало постоянно снимать галочку с файла test/user/user_accounts.txt - он меняется при каждом выполнении тестов.


Добавлю его в svn ignore list.


Готово!


Вот теперь точно Save


Упс :)


Ну update так update


Теперь все пошло нормально. Тортилка классная штука, но иногда бывают зверские глюки.

рисунок 103


Пока я делал эту итерацию у меня появилась идея, а что если выбрать несуществующий вариант ответа?

Анука тестик...


Естественно провалился. Фиксим (означает исправляем, от fix - исправить) код.


Упс! Кажется я жестко промахнулся... В таких случая, когда был поломанный один тест а после изменения испортились сразу много тестов рекомендую делать одну вещь - откат изменений. Никаких Debug и глубоких раздумий на тему, почему не работает и как сделать, чтобы заработало - потратишь много времени. Откат последних изменений, запуск всех тестов, чтобы удостовериться, что все как было раньше и поиск другого места куда вставить новый код.


Для отката рекомендую пользоваться либо средствами SVN либо комбинацией Ctrl-Z - потому как простое удаление с мыслью: "я ведь точно помню, что написал минуту назад" чревато. Если набрать строчку кода, а потом удалить ее - то код изменился 2 раза относительно исходного состояния. Да, именно два раза. А раз изменения делал человек, то он с большой долей вероятности внес ошибку. Если же попросить программу сделать откат (revert svn/Ctrl-Z), то мы гарантированно вернемся в исходное место. Я не параноик, я просто очень внимательный.

Итак попробуем вставить новый код в другое место.


Уже лучше, но все же ожидалось что будет все зелененько. А знаешь почему так случилось? Для написания нового теста я использовал копипаст. Я скопировал тело предыдущего теста и немного заменил внутренности, но забыл поменять тип исключительной ситуации с IllegalStateException на IllegalArgumentException. CopyPast - зло! Там где хочется воспользоваться CopyPast стоит пересмотреть архитектуру и выделить общую логику, которую так хочется скопипастить. Чаще всего помогает Extract method.

Все фиксится довольно просто


Но этот copypast error должен меня стимулировать на устранение дублирования, чтобы в будущем подобного не повторялось. Что общего в этих двух тестах?


А то, что вызывается конкретный метод с неким параметром, в результате ожидается конкретная исключительная ситуация с заданным сообщением, а если нет - то тест слетит с другим сообщением. Попробуем выделить?

Для начала локализируем все переменные.


Лишь теперь еперь можно делать extract method.



После сделаем inline всех локальных переменных


Когда увидел результат, в голову пришла идея немного переделать. Дело в том, что во всех assert в junit принято первым параметром описывать строку, которая вылезет в stack trace если assert не пройдет, а значит надо поменять некоторые параметры местами. Воспользуемся автоматическим рефакторингом, чтобы не наплодить ошибок. Рефакторинг называется change mthod signature.





Так же я не хочу передавать отдельно exception class и exception message - соберу ка я их вместе


Метода такого нет - создадим.


Наполним его смыслом


После этого можно смело запустить все тесты и смело сделать inline крайнего метода



Теперь можно и за второй тест взяться



Вот, она, альтернатива copy past. Долго, согласен, но результат того стоит. Инвестиции окупаются, потому что позже тратишь время на copy past ошибки.

Сейчас я без проблем добавлю новый тест на крайнее значение 0 и -1. Смотри сам


Конечно и тут я воспользовался копипастом :) и это что-то значит. В данном случае дублируются такие строки


Но перед этим я хотел бы исправить код, так чтобы тесты заработали.


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


Осталось пофиксить тесты


и провести устранение дублирования


Все тесты?


Save!



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


Тесты?


Save!



Ну что, теперь можно перейти к вопросу о загрузке информации из xml. Сейчас все захардкоджено в ExamFactory


Предлагаю выделить интерфейс, для этого выделим переменные, которые хотим объединить в объект данных и воспользуемся рефакторингом Introduce parameter object


Упс! Такое иногда случается.


Нам важно сейчас придумать другой способ не делать это ручками, пускай даже полуавтоматический.

Я попробую вначале экстрактнуть установку тех полей, которые я хочу объединить в parameter object, в отдельный dummy метод


А потом сделаю то, что хотел - Introduce parameter object


Изменения меня устраивают!



Теперь можно удалять временный метод qwe


Полученную локальную переменную можно сделать полем класса воспользовавшись рефакторингом Convert local variaable to field


результат меня устраивает!


Теперь если я закомментирую два поля name и questionAnswers то увижу все места, где они используются


Так я могу легко сделать замену на использование данных из нового источника


Я переименую объект данных


Я выделю интерфейс из объекта данных


И меня устраивают изменения!




Теперь я хочу вынести создание экземпляра ExamData в ExamFactroy



А теперь я локализирую все прошитые значения в этот класс..



Теперь надо исправить тест


Нет конструктора? Создадим его



Покуда все уже компилится - запущу все тесты.


А поскольку все работает - сохранюсь...



Поехали дальше. В экзаменаторе прошиты данные экзамена. Непорядок - планирую перенести это в ExamData


Собираюсь решить вопрос таким макаром


Но некоторых методов нет - ничего создадим


Наполним пустышки контентом


Заодно устраним дублирование


После создадим второй метод


Немного подкорректируем


Добавим недостающий метод в реализации и заполним его адекватом



Встрою лишнюю локальную переменную


Все тесты запустил и получил пачку поломатых. Часть тестов из за того, equals не сработал как я того ожидал


Но я его исправлю. Все с этими массивами не как у людей


А вторая пачка тестов поломалась из за того, что я дернул мок за новый метод, а он в тесте пока еще не прописан.


Так пропишем!


И это прокатило, но тест перестал быть модульным. Дело в том, что после получения имени экзамена с помощью ServiceFactory подгружается его данные - а это у нас не замокано.

Вопрос как замокать Factory method?

Создам из фектори объект



Попробуем замокать этот объект в тесте частично, так же замокаем ExamData


А вот как в тесте проходит проверка


Но тест почему-то слетел.


Не хватает класса? Сейчас попробуем отыскать



Вот где класс находится


Теперь надо скачать jar



Добавить себе в проект


В проект, думаю, добавишь себе сам, не в первой


Запустим теперь тесты


NullPoinerException уже ближе...

Где-то что-то не домокалось....

Продебажим?



Ясно - я забыл проинитить новые моки перед тем как их использовать. Сделав это я получаю новое сообщение об ошибке


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


Следующий тест подобным образом исправляем


А вот что касается третьего теста, я не уверен что он вообще нужен - я его собираюсь удалить нафиг - он дублирует уже существующий тест


Теперь что касается дублирования, то в двух тестах что остались я вижу офигески-неприличное дублирование


Выделю ка я этот кусок из одного теста, пока игнорируя незначительные отличия


После выделения я немного доработаю метод


И повторно использую его в другом тесте


Все в шоколаде


А потому Save! Только перед этим я хочу исправить абсолютные пути к библиотекам на относительные. Вот как это выгллядит сейчас (зеленым помечены старые либы с относительными путями - так надо, а красным - новые либы с абсолютными путями - это хочу исправить)


Настройки хранятся в файле classpath в корне проекта. Его достаточно открыть в блокноте и отредактировать, только перед тем стоит закрыть IDE.


В classpath файле как видим находятся ссылки на USER_LIBRARIES.


Сам файл прописан в дебрях Eclipse. Докопаться до него можно. Для этого откроем Eclipse и экспортируем библиотеки в файл.


Теперь можно открыть этот файл для редактирования и исправить все абсолютные пути на относительные



После сохранения файла настроек импортируем его в Eclipse


И проверим результат


Снова все тесты запустим


Зеленое - сохранимся



Продолжим - мы вплотную подошли к тому, чтобы реализовать интерфейс ExamData с чтением информации из xml. Вот этот интерфейс.


Напишем ка на него парочку контрактных тестов (тесты на интерфейс называются контрактными) воспользовавшись схемой наследования предложенной недавно.

Создадим абстрактный тест


А вот и простенький тестик.


Теперь осталось всего лишь реализовать наследника, который определит недостающие абстрактные методы.


И определить методы


Вот его то мы и запустим!


Интересный поворот. Нету поля, значит создадим его!


Конструктор с одним строковым параметром оставим для нормальной работы функциональных тестов - они заточены под те данные, которые прошиты тут...

Запускаем тесты


Упс, ошибочка получилось! Но она легко правится


Теперь можно приступить к написанию реализации, основанной на xml

Создадим вначале тест-наследник со определенными асбтрактными методами. Методы должны будут готовить xml в текстовом виде, сохранять их на диск, после чего вызывать конструктор новой реализации ExamDataXml передавая только название экзамена. По названию будет находится xml, парсится средствами JDom, ну а дальше тесты родителя сделают свое дело...


Реализация будет немного сложнее, чем в прошлый раз, но все же можно разобраться


Пока у нас нет никакой реализации, о чем компилятор подсказывает. попросил IDE создать нашего xml трудягу!


Класс! Подправить надо только папку, в которой он создастся - изначально IDE предложило разместить его рядом с тестами.


Теперь этого малыша надо накормить


Во-первых ему не хватает конструктора


Оу! Совсем забыл - я везде подобавлял throws Exception в абстрактном контрактном тесте, ибо работа с файлами и все такое - а я не хочу обрабатывать эти ситуации, не в тестах по крайней мере...


А все из за этого


Тесты компилятся уже, а значит можем запускать


Имя подправил (там был пробел, а пробел в имени файла - ну его от греха подальше), хотя это натолкнуло меня на мысль - имя файла и имя экзамена не должны быть связаны. О как!


В любом случае этот фикс не помог. Почему? Где мои xml?

Может потмоу что я папку не создал?



Нифига!


А вот Apache Commons помог. Новая реализация сохранения информации в файл.


И все работает! Теперь уж можно и за реализацию браться...

Что-то быстренько наваял, и оно не работает. Ну я и не ожидал большего - хорошо хоть скомпилилось


Не правильно формирую имя файла. Вернее вообще его не формирую.

А так?


Ну вот совсем другое дело.

Так как структуры объектов в JDom я не знаю - мне поможет debug - это наверное единственное место, когда с ним быстрее получается.



Ну вроде как угадал. Тогда глянем в xml файл


Оп! А тут почему name=name?

Ну все понятно


Должно быть так


Второй тест слетел с NullPointer


Вот что значит интуитивно понятный интерфейс - я написал и сразу заработало!


Третий тест постигла та же учесть


Афигеть! Вот к чему стоит стремиться!

Порефакторить что-ли?

Наверное вначале лучше сделать Save :) Помнишь правило простое? Все тесты зеленые - save!


Вначале добавлю в ignore list тестовую папку с xml


Странно - не пускает


Ну ладно потом разберусь...



Итак что нам осталось? Немного причесать код, решить вопрос с (имя_xml != имя экзамена) и переключить фунциональные тесты на xml файл. Совсем немного!
Продолжение следует!

Комментариев нет:

Отправить комментарий