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


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

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

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

Привет. Вот ссылка на прошлую серию. Сейчас мы будем упрощать немного усложняя. Я хочу избавиться от лишних конcтрукторов конфигурирующих другие классы, но в то же время использующися только в тестах. Фу! Spring прикручивать пока очень громоздко, а потому надо написать свой класс, цель которого с помощью reflection вставлять в поля одного объекта ссылки на другие.

Для начала просто удалим то, что нам мешает. С этого почти всегда начинается рефакторинг. Воняет код? Удали его, и подумай, как сделать то же но иначе. Наш экзаменатор конфигурируется сервисом для работы с пользователями. Сделали мы это раньше для того, чтобы в тестах иметь возможность подсунуть mock. Удаляем и сразу видим все места, в которых этот конструктор использовался - его подсветит компилятор.


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


Вслед за этим последовала реализация, достаточная для озеленения тестов.


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


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


Копипастой (или через меню Refactoring) перемести метод в новый класс. Параллельно с этим я, как можно заметить, немного порефакторил метод и добавил в него новое поведение, а именно:
- метод не прокидывает внутренние Exception за свои пределы;
- метод не вставляет ничего в поля с типом Object - ибо туда можно вставить все, что угодно, а это нарушит задумку.
- метод возвращает на выходе тот же объект, что получил на вход удобства ради
- метод типизированный, чтобы предыдущее изменение принесло пользу а не вред - если был бы Object inject (Objct object..., то при пришлось бы в клиенте пользоваться Class Cast чтобы привести результат к исходному типу, а это, согласись, неудобно;
- метод переименован в injectAll, что больше соответствует алгоритму его работы.


Естественно для нового кода нужен новый тест.


Первое и самое просто - это проверка что инъекция состоится, если мы вставляем объект типа А в поле типа А


Еще один тест - проверяем, что объект типа А вставится во все поля типа А


Проверяем, что объект класса А реализующий интерфейс Б вставится в поле типа А.


Проверяем, что объект класса А реализующий интерфейс Б вставится в поле типа Б.


Проверяем, что объект класса А наследующий класс Б вставится в поле типа Б, а так же что объект класса Б вставится в поле типа Б при той же иерархии наследования.


Проверка, что при инъекции не производится вставка в поле типа Object.


Проверяем, что возникает сообщение о не вставке, если присутствует только Object поле.


Проверяем, что возникает сообщение о не вставке, если отсутствуют поля вообще.


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


То же делаем для другого контроллера


И для для реализации экзаменатора


Все тесты?


Зеленые - значит коммитим.



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


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


А потому мы переместим строчку с обработкой вопроса из обработчика режима result в обработчик режима question. Так же в этом обработчике выделим в локальную переменную параметра риквеста answer - в будущем мы будем проводить его анализ.


А анализ такой, что если нет никакого answer в request, то мы не будем делать ничего, что раньше делали в processQuestion. Это делается для того, чтобы избежать обработки несуществующего вопроса, который еще не готов до момента отображения первого вопроса.


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


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


Теперь смело можно избавиться от бессмысленной локальной переменной.


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


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


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


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


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


Оказывается в реализации метода hasMoreQuestions надо было продумать, что при первом вызове он должен вернуть true, а при последующих false. Так как билет (ExamQuestionAnswer) свое состояние хранит, то я попрошу подсчитать количество вызовов и в зависимости от его значения буду возвращать либо true либо false.


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


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


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


Новое поведение проверки есть ли еще вопросы так же выделим в более информативный метод


Итого тест позеленел!


Остается еще один, а именно, когда вопросов больше нет и пользователь видит страничку результатов.


Легким движением руки.... 


Теперь очевидно нам больше ненужна ветка else if для режима result.


а метод processQuestion я решил переименовать в processAnswer


В тесте я увидел дублирвоание, но устранять я его собрался не в тесте а в тестируемом методе - дело в том, что это unit тест, тестирующий белый ящик, а значит дублирование в нем указывают на дублирование в в этом самом ящике. Устраним дублированеи у тестируемом методе, удалится оно и в тесте.


Для начала я перемещу еще выше строчку с получением экзамена, и передам его во все места, в которых требуется наличие билета. метод processAnswer раньше сам получал экзамен из сессии, а теперь ему этого делать не надо. То же касается метода doResult.


А теперь можно удалить дублирование и в тесте. Результат - все зеленое.


Еще я заменил два вызова одним, просто переместив его туда, где ему место


Снова блок, выделенный пустыми строками, которому я могу дать информативное имя - выделяю


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



В результате тест стал очень даже читабельный с точки зрения пользовательских историй. Посуди сам: для режима question, это первый вопрос, при этом экзамена нет в сесии, есть еще вопросы, готовим форму с вопросом, идем на jsp. Или:  для режима question, сохраняем ответ, еще есть вопросы, готоим их к отображению, отображает на jsp. Как по мне очень ничего.


Еще один блок - выделяем.


Все тесты зелененькие! 


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



Начинаем следующую итерацию после перерыва с чего? Правильно - с запуска всех тестов.


Где хранится наш вопрос? Он размазан по get-ерам класса, его инкапсулирубщего .

Создадим из него копию


И добавим суффикс multiple


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


Эти пару методов, я выделил в отдельный интерфейс.


IDE предложила занаследоваться, но я тут предпочитаю аггрегаию, потому что список действий (или стратегия поведения класса) ExamQuestionAnswer не является ExamResult, но может его включать.


Заменим во всех местах, на которые укажет нам компилятор. Тут добавим:


Тут реализуем новый метод, а старые удалим


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


Реализации результата нет - создаем.



Теперь дело за тестами. Моки надо немного перепрограммировать


Юнит тесты заработали. Пошли дальше.


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


Новый метод интерфейса результаты


Еще один метод в реализации


Попробуем реализовать результаты в первом приближении


Но тесты не проходят

исправим


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


Выглядит неплохо. Подключим его взамен старой, одновопросной, реализации.


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


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


Попробуем решить иначе и получаем еще один зеленый тест.


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


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


Все супер!


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


естественно они не сработают, а потому разремарим два новых вопроса в многовопросном экзамене


Уже лучше, но все же немного не то


А все дело в том, что надо поменять старую логику проверки одного вопроса экзамена на проверку трех


А это ведет к расширению интерфейса результата экзамена новым методом getAnswers


Старый при этом можно удалить


Наведем порядок в реализации



Я решил, что Integer[] будет лучше, чем int[] потому, чтоне хочу тратить много времени на поиск ответа, как настраивать мок с помощью EasyMock если у нас массив примитивов. Фу, но так быстрее. Потом исправим. Рефакторинг я люблю, а потому без угрызения совести оставляю подобные кизячки на потом. Кизячком я это считаю потому, что из за тестов мне пришлось менять код.

Подправим еще и unit тесты


И экзаменатора в связи с изменением интерфейсной части


Все тесты?


Зеленые! Коммитимся, я все сделал, что хотел.



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


Одна простая валидация на то, залогинен пользователь или нет и вылогинивание пользователя, если он на страничке result и тест прошел


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


Немного подумал и понял, что надо так же из сессии вычищать не только user name но и его старый экзамен, иначе второй пользователь, если попытается пройти экзамен - будет работать со старым экзаменом, а там и отвечать-то нечего - как результат сразу страничка с результатами :)


Раз эти две строчки спаренные, то я их выделю в метод logoutUser.


Еще один Extract method для проверки если юзер залогинен


Конечно же поломаются unit тесты - они всегда ломаются, когда меняются внутренности тестируемых ими методов - фишка тестирования белого ящика.


Программируем новое поведение


А вот с этим тестом не так все просто.


Вроде и logout сделал, как положено, но все же где-то ошибся.

Выделю ка я в локальную переменную получение сессии


И все заработало!


Все тесты так же зеленые


Я добавлю еще один unit тест проверяющий, что незалогиненный пользователь не может ничего делать с контроллером.



И еще один функциональный тест, который говорит что если юзер не выбирет варианты ответа и попробует так отвтеить, то ему предложит тот же вопрос


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


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









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








Ну и дальше просто экстрактим...


Подправил название


Вот как выглядит результат


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



На сегодня все. Не переключайтесь...

2 комментария:

  1. Санёк, это было жестоко... Я палец смозолил, пока скроллил.

    ОтветитьУдалить
  2. Спасмбо за веселый коммент :)
    Да было дело. Писал такие монстры. Руку отмозолил, и быстренько получалось. Сейчас все больше видео снимаю - эффективнее оно, хотя если не с первого дубля, то озвучка и монтаж потом отнимает время соизмеримое с созданием скриншотов. Давно вообще это было. Я эти посты вчера достал из черновиков. По 2 года им. Время как летит...
    Если не пользу учащемуся, то хоть троллей отпугивать будут - и то польза.

    ОтветитьУдалить