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


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

воскресенье, 20 января 2013 г.

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

Привет! Линк на прошлую серию тут. А сегодня история, которую мы реализуем, звучит так: "Появляется возможность работать с несколькими экзаменами - на странице выбора экзаменов теперь отображается список." Я ее разбил на три составляющие, потому что по моей предварительной оценке она занимает слишком много времени. Все что сложнее логики логинки более чем в 4 раза - все стоит разбивать на подзадачи.

Подзадачи я вижу такие: 
1. Теперь о юзере может храниться более сложная информация а не просто пароль/сдал/не сдал. Пускай это будет тот же properties файл, но немного другого формата.
2. Изменение части контроллера - надо научить его понимать какой экзамен пользователь выбрал и от этого подгружать необходимую xml с экзаменом. Тут же решение задачи, что название экзамена != названию xml файла с экзаменом.
3. Получение списка всех экзаменов по существующим xml файлам. Тут же подгрузка только тех экзаменов, которые студент еще не прошел.

Начнем с первой.

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

Сейчас в файле с описанием пользователей информация хранится в таком формате <имя пользователя>=<пароль>|"FAILED"|"PASSED"


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


Но для начала запустим все тесты:


Все зелено? - продолжаем...

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


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


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

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



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





А после снова выделим все тесты, использующие поле users, из наследника новоиспеченного UserServiceTest и применим к ним рефакторинг PullUp.


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


Потом еще раз Next.


Подождем, это займет некоторое время


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


Проглядываем что произошло в результате рефакторинга (может чего пошло не по плану?) и ждем Finish


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

Конкретика относительно структуры файла останется в наследнике


А все, что зависит от интерфейса UserService - уйдет в абстрактного родителя. + Родитель компилировать не будет, потому что там нет никакого упоминания о users поле.


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


Это нам слабо помогло избавиться от ошибок компиляции, но минутку терпения. Добавим теперь метод getDefaultUsers и заменим во всем классе users на вызов этого метода, но ВНИМАНИЕ!, только в тех тестах, которые users используют один раз. В тех тестах, которые используют более одного раза users надо добавить одноименную локальную переменную, значение которой получить вызвав тот же метод getDefaultUsers.


Замечу, что default данные мы украли из наследника


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


Добавим его тут как абстрактный.


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


Осталась разобраться с наследником-конкретикой. В нем как минимум две ошибки компиляции.


IDE нам поможет. Вначале нам надо модифицировать метод assertThatUserPassed так, чтобы он стал реализацией одноименного абстрактного метода из родителя.

Первый шаг - убрать ключевое слово static.


Следующий шаг - сделать методы похожими по типу прокидываемых исключительных ситуаций.


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


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


Итак почти все готово, осталось только доделать getUsers метод


Раньше эту роль выполняли initUserService и makeUsersFile методы вместе. Перенесем их реализацию в getUsers


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



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


В ходе добавления я заметил, что метод assertExceptionWithMessage кажется не на своем месте, т.е. ему тут не место. Перенесем его в какой-то TestUtils. Для этого создадим пустой класс в пакете test/utils.


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


последовало бы ручное изменение всех вызовов с учетом нового местоположения. Фуу.. А если ошибку какую-то внесу? Я и так долго тесты не запускал...

Итак попросим IDE cделать перенос за нас. Поставим курсор ввода на название метода в исходном классе и выполним рефакторинг Move (горячая клавиша Alt-Shift-V или меню Refactoring->Move...).

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


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


А после повторить Рефакторинг Move. Теперь IDE нам предлагает варианты - укажем класс, куда хотим перенести метод и посмотрим от чего IDE нас избавило





IDE даже в разделе import порядок навела. Супер! Вот, что мне было делать лень. Жмем OK и продолжаем


Следующая трудность не такая простая.


Дело в том, что мы прокидывали везде Exception, а для тестирования исключительных ситуаций используем анонимную реализацию стандартного Java интерфейса Runnable - в его сигнатуре не указано throws Exception и поменять мы его не можем.


Но мы можем написать свой! Сделаем это в том же TestUtils классе.


Теперь для каждого теста достаточно будет сделать так


Но вручную я сделаю это всего раз (ленив, помните?) - все остальное я попрошу сделать IDE используя замену по файлу.


Риcунок кишит пометками, потому я его поясню. Дело в том, что в разработке пользоваться мышкой - это все равно что спринтеру привязать к ногам 10кг свинцовые гири и попросить пробежать дистанцию на время. По этой причине везже где только можно я пользуюсь горячими главишами. В принципе мышка нужна только для того, чтобы добраться в соответствующее меню и подглядеть какая там стоит клавиша, а после юзать только ее. Replace All по всему файлу я использую довольно часто, а потому давно делаю это без участия мышки.

Всего в этом процессе 6 этапов:
1) Выделяем строчку, которую только что исправили ручками. Но не мышкой выделяем а, поставив курсор в начало строки нажатием Home с последующтми нажатием Shift-End.
2) Копируем строчку в буфер обмена нажатием Ctrl-C (или Ctril-Ins кто как привык)
3) Перемещаемся к следующему методу, содержащему заменяемую строчку нажатием Ctrl-Shift-Down, а потом к самой строчке (Down, Down, Down)
4) Выделяем и это строчку нажатием Home, Shift-End.
5) Вызываем диалог Поиска/Замены нажатием Ctrl-F
6) Выделенный текст автоматически скопируется в поле Find (собственно за этим его и выделяли на шаге 4) а мы нажмем Tab, чтобы проскочить это поле
7) Вставим ранее скопированную строчку-заменитель нажатием Ctrl-V (или Shift-Ins кому как удобнее)
8) Нажмем на клавишу Replace All? но НЕ МЫШКОЙ! :) а подглянув, какая буква в надписи на кнопке подсвечена и нажав на нее с Alt кнопкой (в данном случае Alt-A)

Итого комбинация Home, Shift-End, Ctril-Ins, Ctrl-Shift-Down, Down, Down, Down, Home, Shift-End, Ctrl-F, Tab, Shift-Ins, Alt-A

Длиннющая! Но запоминать ее всю не надо, дело в том, что регулярно пользуясь горячими клавишами вскоре это войдет в привычку и ты не будешь задумываться о том, что нажать, а всего лишь будешь думать о том, что делать, а руки, в свою очередь, сами нажмут то, что надо. Если сделать горячие клавиши ежедневной привычкой, то вскоре окружающие будут удивляться как молниеносно ты работаешь - и это проверенный факт. У меня процесс замены с горячими клавишами занял 4 секунды. Если бы со мной кто-то сейчас сидел в паре, то вряд ли бы он понял, что я сделал - хотя бы потому, что диалоговое окно поиска/замены мелькнуло на секунду и сразу пропало. Кстати с мышкой тот же процесс занял 24 секунды.

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

Вот она, вредная, зависимость


Можно исправить код и оставить зависимость, но я предпочитаю устранить ее. Вопрос в том, как?

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


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

Вот вторая бяка!


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

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

Кстати, наше превращение простого теста в контрактный не сделало его модульным. UserServiceFileTest по прежнему тестирует UserServiceFileImpl вместе с Properties классом и файловой системой. Функциональный тест, косящий под модульный - интересно звучит.


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

Некоторые классы в проекте не компилируются, об чем сообщит мне IDE но я попрошу ее проигнорить это.



Зеленый? Это хорошо, но это намекает на то, что надо сделать commit. А потому я вредные зависимости на время оставлю в покое, то есть верну public на место и статический доступ заменю еще более некрасивым динамическим.


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

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


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


Но это опять поломает нашего вездесущего клиента


Выхода пока нет - передадим ему исходные данные (их я скопипастил из контрактного теста).


Теперь, пока весь проект компилится - я могу запустить все тесты.


Все зеленое? Сохраняемся. А то, что стоит сделать дальше - записано в TODO: устранить вредную зависимость. Вернемся к ней, когда будет настроение.



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

Если разделить эту задачу на две:
1) изменение формата, но экзамен остается один
2) добавление возможности сохранять информацию о нескольких экзаменах для каждого юзера.
то мы сможем реализовать первую, без добавления новых тестов, т.к. задача по сути - рефакторинг, а вот вторая - это уже расширение функциональности - а это, в свою очередь, требует написания теста-требования перед изменением в коде (помнишь, договаривались? Утром требование, вечером код!)

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



Тесты не повалились, а потому я закомичу эту правку.



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

Что если из UserServiceFile сделать singleton а UserFactory будет управлять его созданием. Загрузка из файла будет во время инстанциирования в конструкторе UserServiceFile, а сохранение данных в файл всякий раз, когда это понадобится. Неее...

А что, если мы разделим класс UserServiceFile на два класса - первый будет работать с данными о пользователях инкапсулированных вместе с методами загрузки в файл во втором классе. А еще я узнал, что Properties это реализация Map. И что, если теперь полем класса будет этот самый Map, и лишь на момент загрузки/сохранения будет известно что это Porperties?

А может попробовать наконец-то создать класс User, инкапсулирующий в себе все свойства пользователя, UserService будет хранить в себе Map этих User данных, а третий класс, допустим UsersSaver будет сохранять Map в файл любыми доступными средствами. UserService знает про User и UsersSaver. Не сложно ли? А попробуем!

Я создам новый тест для этих случаев, а так же новый набор рабочих классов. Но начнем с теста.

Для создания класса есть хорошая горячая клавиша: Alt-Shift-N,C ;)



Наполним его смыслом. Тут будем писать код так, как будь-то бы он уже существует. Компилятор будет ругаться, и мы попросим IDE создать все необходимое за нас.


Много ошибок! Создадим для начала интерфейс User



Продолжение следует…

7 комментариев:

  1. Очень интересно Саш! Пока дочитал до половины.
    Кажется для Refactor Move там достаточно было статики - если метод приватный эклипс сделает его паблик или пакетным по необходимости

    ОтветитьУдалить
  2. Спасибо Лёня. Рад, что пригодилось.
    По рефакторингу в эклипсе ответить не могу ничего пока. Я уже больше году на Idea работао, и про Eclipse подзабыл всякие-разные тонкости...
    Знаю только, что в Idea рефакторинг в разы приятнее.

    ОтветитьУдалить
  3. А продолжение будет???

    ОтветитьУдалить
    Ответы
    1. А надо?
      Есть пару вопросов:
      - вы пробовали практически что-то сделать или только читаете?
      - удобен ли такой формат или лучше видеокасты записывать?

      Удалить
    2. Вот, пожалуйста TrotiseSVN реппозиторий со всеми ревизиями (Eclipse) и последняя ревизия, мигрированная уже на Idea.

      Кстати, одно предостережение. Сейчас уже есть всякие разные Spring MVC и тому подобные удобные фремфорки. Статья была написана пару лет назад, а потому могла чуть устареть. Что в ней не устарело - ТДД (он за это время не поменялся ни сколько), мой подход к декомпозиции проблем (может быть кому-то будет полезным), ну и базовые вещи в Web - они так же не имезнились.

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

      Если очень надо могу как-то и продолжить...

      Удалить
  4. Спасибо! очень инетерсно! только я про хадсон ничего не нашла(
    напишите, если не сложно) очень интересна непрерывная интеграция и тесты. можно напримере любой системы непрерывной интеграции.

    ОтветитьУдалить
    Ответы
    1. Да это так. Не дошел до этого т.к. команда состояла из одного разработчика - меня, надобности в CI небыло.
      Сейчас этот проект приостановлен на неопределнный срок, потому могу посоветовать статьи в сети на тему CI
      например вот эту http://www.vogella.com/articles/Jenkins/article.html

      Удалить