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


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

среда, 13 июня 2012 г.

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

В прошлый раз мы написали unit-тесты для Exam контроллера.

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

Чтобы задать новое поведение экзаментатора я создам новое требование для соответствующего модуля (то бишь юнит тест). Для этого нам понадобится новый класс - создадим его.


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


Для этого воспользуемся автоматическим рефакторингом и переименуем метод экзаменатора в соответствии с новой ролью


Можно так же проглянуть все места, которые зацепил данный рефакторинг перед тем как его применить


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

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


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

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

Вначале создадим первый; после немного подкорректируем интерфейс; затем реализуем пустышку в сервисе, реализующем изменяемый интерфейс; наполним пустышку смыслом, добавив соответствующее поле. Закрутил? Думаю, картинки ниже все пояснят


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


А вот и связь между экзаменатором и userService. Экзаменатор как и раньше делает проверку результатов экзаменуемого (помечено синеньким), делает пометку в журнале-userService (помечено красненьким) и возвращает результат тому, кто его запросил (помечено зелененьким).


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

Пробуем теперь запустить требования, и видим fail.


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

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


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


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


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


Сейчас можно решить проблему двумя способами:
1) просто скопировать недостающую строчку, а потом вставить ее куда надо (часто говорят, скопипастить, от Copy-Paste)
2) немного сложнее, но через рефакторинг выделить недостающую логику в отдельный метод, с последующим его повторным использованием.

Пойдем вторым путем. Чтобы проделать выделение метода (Extract Method) нам надо выделить локальную переменную (она будет отличаться в различных тестах)


После смело можно выделить метод


Выделение локальной переменной было воспомогательным действием - более мы в ней не нуждаемся, а потому смело можем встроить ее (Inline)


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


Теперь поглядим не поломали ли мы чего еще?


Нет? Замечательно!

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


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


Теперь все работает! Информация о пользователе сохраняется, после чего он больше не может залогинится


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


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


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

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


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


Вот так-то лучше!


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

Для начала я выделю методы загрузки и сохранения файл 




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


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


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


Косметический рефакторинг - выделим дублирующиеся строки в отдельные константы


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


Причем подключаю тяжелую артиллерию - отладку (debug). Так я точно найду в чем дело, но времени у меня это отнимет достаточно. Debug - самый тяжеловесный в плане потребления времени инструмент. Debug в отличие от теста/требования нельзя повторно использовать.




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

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


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



Не доходит. Поднимаемся на уровень выше и поглядим располагает ли контроллер этим именем?


Нет! Оказывается в сессии после залогинивания нет информации о пользователе, а потому ExamController передает null экзаменатору, тот ставит null в билет, и в конце концов userService делает с этим null что-то недопустимое.

Исправление мне видится таким - в LoginController при успешном залогинивании добавляем имя залогиненого пользователя в сессию.


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


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

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


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


Кроме того это дублирование, которое я хотел бы устранить. Любое дублирование должно быть устранено. Тесную взаимосвязь между LoginController/ExaminatorImpl и специфично настроенным UserServiceFile мы устраним с помощью так называемой фабрики. Теперь клиенты нашего UserService для получения рабочего сконфигурированного экземпляра будут просить об услуге UserFactory, а тот в свою очередь будет делать всю необходимую настройку.  Создадим новый класс UserFactory


Пока он пуст


Воспользуемся рефакторингом Introduce Factory, который сделает за нас всю грязную работу - спрячет инициализацию UserService в методе createUserService новой фабрики.


Вот так


Немного не то, что я хотел, а потому удалим лишний параметр


Создадим константу, указывающую на файл с пользователями



скопируем туда исходный путь


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


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


А так же доступной для изменения



Ну хоть это исправили! Старые тесты остались как и раньше нерабочими.


Работая с тестами/требованиями важно замечать малейшие изменения, которые происходят при изменении кода - они верные индикаторы.

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


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


И поглядим что выйдет


ы забыли о правилах junit 4.0 - метод аннотированный как Before должен бть динамическим, а потому удалим модификатор static



Выделим метод инициализации пользователей


Вот так


И вызовем его из функционального теста логинки



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


Хух! Наконец-то мы починили тест. А ты еще помнишь что мы его чинили? В разработке часто надо помнить, что ты делал до того как ты начал делать то, что привело тебя к необходимости делать что-то третье. Сложно? Тот то же. Я пользуюсь простым карандашом и листком бумаги. Если держать все в голове, а думаю, что ты так и делаешь - все может закончиться плачевно: твой напарник проходя мимо тебя вдруг захочет тебе рассказать что-то очень важное но совершенно бесполезное в контексте твоей задачи - это будет неожиданно и скорее всего ты некоторое время будешь тупо смотреть на напарника продолжая по инерции работать над своей программой, а он будет что-то говорить. Тут ты поймешь, что надо переспросить его - а повтори еще раз, и скорее всего из вежливости сделаешь это. Все, забудь про то, что ты делал - вернее ты уже забыл все, что делал до сих пор, на чем остановился и что ты делал до того как ты начал делать то, что привело тебя к необходимости делать что-то третье, что делать дальше и вообще. Вернуться назад будет крайне сложно - придется восстанавливать всю цепочку причин-следствий и скорее всего что-то будет упущено. Чаще всего упускаются из виду самые последние и незначительные изменения в коде. Так закладываются бомбы, взрыв которых спустя некоторое время отнимет у тебя много времени на потраченный дебаг. Кто виноват, напарник, который захотел минутку общения в неподходящий момент, или ты, стремящийся все удержать в голове?  Ладно понесло Остапа...

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


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

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



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


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


Метод пропадет в LoginTest и появится в FunctionalTest, а так как у них родственные связи, то фактически он останется в LoginTest. Фишка в том, что так он появится и во всех других наследниках FunctionalTest. Наследование, блин.


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


Исправляется простым добавлением мока сессии с программированием его поведения


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


Пока тесты пустые - подготавливаются только исходное местоположение.

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


Вот как это выглядит в результате. Симпатично


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




Вот что я хоте увидеть в результате


С помощью этого так же упростились и другие тесты. Читабельнее стало, не так ли?


Еще один метод мне понадобится - метод проверки что залогинивание успешно прошло


И метод, что залогинивание не удалось


Вот все, что я выделил. Запустив тесты удостоверился, что все работает как и прежде


Теперь можем просто наполнить наши тесты проверками


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

Нам потребуется новый метод проверяющий что линк на логинку есть на страничке



и мтеод перехода по этому линку на логинку


Вот они


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


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


Все тесты прошли! Замечательно.


Идей больше нет, а потому я сохраню этот кусок работы.



Дальше лучше, не отключайся...

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

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