Привет. Вот ссылка на прошлую серию. Сейчас будем делать админку.Админка - это такая страничка для администратора, на каоторой он может гибко настраивать программу. В нашем случае надо сделать так, чтобы у админа была возможность добавлять новых пользователей, желающих пройти экзамен. Кроме того у админа особенные права - при залогинивании он видит сою страничку, а не список экзаменов, ну а пользователь, в свою очередь, не имеет доступа на админку. Как-то так.
Все начинается из сердца Web приложения - файла web.xml в который мы добавим новый сервлет. Тут все просто - если в браузере теперь ввести http://localhost/admin, то управление примет сервлет, описанный в классе AdminController.
Класс этот нам надо создать
Его метод doGet вызывается в ответ на подобные запросы из адресной строки браузера (т.е. введем http://localhost/admin и вызовется этот метод). Есть еще и doPost метод, который может вызываться, если на /admin отправляется html форма методом POST.
Реализация этого метода пока простая - отобразим для начала jsp страницу содержащую форму для добавления нового пользователя.
Теперь создадим jsp
Я предпочитаю копировать имена файлов и прочие данные, которые встречаются в двух разных местах по той причине, что вводя дважды "addUser.jsp" я могу в одном из низ ошибиться. Такие ошибки неприятны и их достаточно сложно отловить (по той простой причине, что "ну я же точно не ошибся!"). Правда copy past ведет к другому типу ошибок - очепяткам, которые потом остаются в коде. Например я могу написать сразу "addUger.jsp" и так бы оно и осталось. Но я пользуюсь рефакторингом и в тот момент, когда подобная очепятка будет замечена, она будет устранена. Так что я не парюсь по этому поводу и использую менее патогенный способ.
Страничка должна содержать форму с двумя полями и кнопкой отправить.
Режим add_user я добавил для того, чтобы в одном и том же методе doGet отличать режим - добавление нового или первый показ формы?
Теперь можноподружить контроллер и сервис пользователей, который мы сейчас научим создавать пользователей.
Вот он в интерфейсе
Создадим метод-пустышку
Неприятно на факт - во всех классах-реализациях, при добавлении нового метода в интерфейс, надо добавлять и реализацию нового метода.
Наполним пустышку смыслом
Выделим локальную переменную
Вставим новый код на логинке - если админ, то идем на админку, иначе как обычно
Тестов нет - запускаем сервак и тестим мануально, то бишь ручками
Вводим admin admin. Наверное процентов 80 админских паролей во всем мире это логин=пароль.
Что-то пошло не так
Ну да, метод doGet мы добавили, а doPost нет. Добавим и его. Нам все равно каким методом пришел запрос - GET или POST - действие будет одинаково, а потому я использую делегирование из doGet в doPost
Снова мануальное тестирование
Круто! Теперь форма отобразилась!
Но пользователь все так же не сохранился, ибо NPE (NullPointerException)
По логам видно, что где-то тут
Продебажим...
Для этого в строчке, вызвавшей ошибку поставим точку останова.
Запустим jetty server в режиме debug
Отправимся на админку
И попробуем создать пользователя
Как оказалось userName is null. А почему?
Поднимемся выше по stack trce и посмотрим что там, а там в риквесте парамеnh user_name is null
А на форме и на логинке мы работаем с другим параметром - username. Вот она! Исправляем ошибку.
И снова запускаем сервер, чтобы в очередной раз потестировать ручками
Все сохранилось - если посмотреть в файл с описанием пользователей, то там мы увидим данные, введенные на форме.
Создадим теперь тест для этого всего, а то ручное тестирование меня не привлекает
Создадим функциональный тест, на котором просто проверим, что если мы ввели admin/admin на логинке, то отправляемся на форму добавления пользователей.
Те методы, которые мы недавно выделяли теперь нам нужны в другом месте, а потому сделаем их публичными и статическими
Второй тест проверяет как создается полноценный пользователь, под которым после можно залогиниться
Дело за модульным тестом
Так же два теста, на тех же два случая
Для UserService у меня так же есть парочка тестов. Один, самый простой, мы реализовали а вот остальные, чтобы не мозолили мне мое ОЗУ, я вынес в виде комментариев.
В логинку так же добавлялся новый код - проверим это
Странно, но почему-то поломался функиональный тест админки.
Ах ну да, я допустил ошибку. тест раньше запускался со своим напарником, который приводил к страничке добавления нового пользователя, а потом наш тест делал свое дело. Теперь тест запустили отдельно и он не увидав своей странички добавления пользователей слетел. Лечится просто
Теперь стоит проверить, что незалогиненный пользователь не имеет права доступа к админке
Помнится я когда-то это проверял в контроллере ExamController.
Скопипастим (походу оставляя пометку где-то, что дублирование надо устранить) оттуда
Раз клиентов у метода isUserLoggedOut теперь два и находятся они в разных местах, то новое место надо искать и для этого метода. Вырезаем его
И переносим в контроллер логинку, а так же делаем статическим и публичным (не нравится мне это, но пока это единственное что пришло в голову и делается быстро)
Меняем в клиенте адресат
Вуаля - тест прошел
Но поломалось другое
Анализируем поломку
Непонятно. А второй тест?
Непонятно - включаем debug...
Идем глубже
Ну вот, в сессии нет атрибута с именем user_name
Все дело в том, что при залогинивании как админ мы не сохраняем в сессии этого админа, а потому проверка на то, а не залогинен ли пользователь, будет всегда возвращать false.
По этой причине немного переделаем контроллер логинки и тест заработает
В скриншоте голубенькие линии говорят, что строчка быладо изменения в классе, но поменял свое местоположение. Зелененькие линии - код не менялся. Красные линии - код был добавлен.
Идем дальше - модульные тесты естественно полетят
А потому мы еще немного пошаманим - так логичнее, чтоли
Походу я хочу инкапсулировать в UserService логику проверки пользователь админ или нет.
Новый метод в интерфейсе
Новая пустышка в реализации
Ну и в соке, блин. Может удалить его?
Наполняем смыслом пустышку (код раньше был в контроллере)
Снова что-то моломалось
Получается я ожидаю что админ залогинился, а его отправили на страничку с сообщением что он неправильно что-то.
В коде это говорит о том, что checkUser с аргументами admin admin возвращает false
Таки да. Исправим это недоразумение
Тест заработал.
Теперь я хочу немного порефакторить код тестов. Для начала трехэтапный экстракт
Вводим поясняющую константу
Тут же добавляем ее брата
Во всем наборе тестов теперь можно избавиться от лишних строк.
Еще две константы
Расширим метод добавлением нового аргумента
Банальное переименовывание
И немного доработки. Три теста работают
Что же с другим?
Ожидаетсяпереход на админку, но этого не происходит. Почему? Да потому что моки не настроены правильно.
Сделаю как я еще в одном месте выделение метода
Этот метод нам понадобится тут
Тест зеленый. Inline и готово!
Продолжаем выделение методов
Я немного подумал и решил, что выделять стоило две строчки а не одну. Перемещу, пока не поздно
Теперь можно сократить код
А главное тесты зеленые
Так, что у нас с unit тестами админ контроллера?
Ожидается не то, что есть на самом деле, а потому немного extract method
Я уже созрел для выделения части общих методов юнит тестов в абстрактный класс
Пускай это будет ControllerTestm, и перемести мы туда все, что касается контролирования в сервлетах
Тесты зеленые. Замечательно!
Вот как выглядит класс, от которого я собираюсь наследовать все наборы тестов для тестирования контроллеров
Сделаем это для тестов логин контроллера
Немного доработки, ибо расширяем
Порядок так же стоит навести и в самом родителе. Раз он абстрактный, то ничего не должен знать про конкретную версию контроллера. так же модификаторы доступа сделал всем мокам protected, дабы наследники могли видеть их как свои
Основная причина ошибок - мок добавили, а проинитить ипровалидировать его забыли.
Чистим тесты для админ контроллера
Вот теперь самое интересное. Метод приватный, а значит надо либо менять модификатор доступа (что ради тестов не очень хорошо)
Либо использовать рефлексию java
Теперь надо во всех тестах прокинуть все эксцепшены вверх. Один раз сделаю, и больше не буду париться. Можно было написать try/catch с оборачиванием всех checked Exception в unchecked RuntimeException, но я не стал этого делать.
Почему-то NPE. Почему?
Снова дебажим
controller is null. Почему?
Добавлю ассерт, чтобы информативнее было на будущее
Ах да, я забыл проинитить контроллер и вставить в него все, что надо. Но почему-то вставка userService не удалась
Все потому что copy past - я тупо не то инджектил
Пошли дальше. Тест админ контроллера
Можем легко переписать с использованием новых методов родителя
Итого все тесты зеленые
Теперь можно дописать все тесты, которые я оставил в виде комментариев раньше
Добавим новый тест - простой пользователь не имеет права доступа к админке.
Добавление валидации в контроллер привело к озеленению нового теста, но поломало старый
Первое, что я провтыкал - это то, что пользователь хранится в сессии а не в риквесте.
Посуди сам. Логин контроллер делает всю небходимую валидацию и его казалось бы достаточно, если бы не хитрый залогиненный пользователь, который вдруг попробует перейти на админку - для этого уже на админке потребуется проверить а не адмил ли это, и если это так, то отправить снова на логинку. Поэтому удаляем лишний параметр везде, где надо
Естественно это повлияет на изменение интерфейсной части
и всех тех мест, где этот интерфейс используется
Это даже приведет к удалению неактуальных тестов
и изменению мока userService
Изменится и основная реализация
После того, как все ошибки компилятора будут улажены можно запустить тесты с надеждой что все будет хорошо
А нет! Гипотезы у меня нет, а потому включаем debug
Снова я ошибся с именами параметров сессии?
Да.
Исправление повлечет за собой озеленение пачки тестов, но некоторые все же останутся нерабочими
А все дело в том, что мы используем пользователя sasha для тестирования админки. Непорядок!
Исправление помогло.
Немного поэкстрактим
И переместим новый метод в родителя. Это метод - помощник при тестировании контроллеров - там ему и место
Два новых модульных теста для админки. Один сразу работает, а другой нет
Моки придется перепрограммировать
Естественно вслед за этим последует extract нового метода
Теперь можно использовать новый метод во всех местах
Я так же забыл переместить константы в родителя
Запустим-ка все тесты?
Зеленые? Коммитимся!
Это был большой кусочек, но зато теперь у нас (вернее у того кто знает админский пароль) есть возможность добавлять новых пользователей.
Продолжение следует...
Все начинается из сердца Web приложения - файла web.xml в который мы добавим новый сервлет. Тут все просто - если в браузере теперь ввести http://localhost/admin, то управление примет сервлет, описанный в классе AdminController.
Класс этот нам надо создать
Его метод doGet вызывается в ответ на подобные запросы из адресной строки браузера (т.е. введем http://localhost/admin и вызовется этот метод). Есть еще и doPost метод, который может вызываться, если на /admin отправляется html форма методом POST.
Реализация этого метода пока простая - отобразим для начала jsp страницу содержащую форму для добавления нового пользователя.
Теперь создадим jsp
Я предпочитаю копировать имена файлов и прочие данные, которые встречаются в двух разных местах по той причине, что вводя дважды "addUser.jsp" я могу в одном из низ ошибиться. Такие ошибки неприятны и их достаточно сложно отловить (по той простой причине, что "ну я же точно не ошибся!"). Правда copy past ведет к другому типу ошибок - очепяткам, которые потом остаются в коде. Например я могу написать сразу "addUger.jsp" и так бы оно и осталось. Но я пользуюсь рефакторингом и в тот момент, когда подобная очепятка будет замечена, она будет устранена. Так что я не парюсь по этому поводу и использую менее патогенный способ.
Страничка должна содержать форму с двумя полями и кнопкой отправить.
Режим add_user я добавил для того, чтобы в одном и том же методе doGet отличать режим - добавление нового или первый показ формы?
Теперь можноподружить контроллер и сервис пользователей, который мы сейчас научим создавать пользователей.
Вот он в интерфейсе
Создадим метод-пустышку
Неприятно на факт - во всех классах-реализациях, при добавлении нового метода в интерфейс, надо добавлять и реализацию нового метода.
Наполним пустышку смыслом
Выделим локальную переменную
Вставим новый код на логинке - если админ, то идем на админку, иначе как обычно
Тестов нет - запускаем сервак и тестим мануально, то бишь ручками
Вводим admin admin. Наверное процентов 80 админских паролей во всем мире это логин=пароль.
Что-то пошло не так
Ну да, метод doGet мы добавили, а doPost нет. Добавим и его. Нам все равно каким методом пришел запрос - GET или POST - действие будет одинаково, а потому я использую делегирование из doGet в doPost
Снова мануальное тестирование
Круто! Теперь форма отобразилась!
Но пользователь все так же не сохранился, ибо NPE (NullPointerException)
По логам видно, что где-то тут
Продебажим...
Для этого в строчке, вызвавшей ошибку поставим точку останова.
Запустим jetty server в режиме debug
Отправимся на админку
И попробуем создать пользователя
Как оказалось userName is null. А почему?
Поднимемся выше по stack trce и посмотрим что там, а там в риквесте парамеnh user_name is null
А на форме и на логинке мы работаем с другим параметром - username. Вот она! Исправляем ошибку.
И снова запускаем сервер, чтобы в очередной раз потестировать ручками
Все сохранилось - если посмотреть в файл с описанием пользователей, то там мы увидим данные, введенные на форме.
Создадим теперь тест для этого всего, а то ручное тестирование меня не привлекает
Создадим функциональный тест, на котором просто проверим, что если мы ввели admin/admin на логинке, то отправляемся на форму добавления пользователей.
Те методы, которые мы недавно выделяли теперь нам нужны в другом месте, а потому сделаем их публичными и статическими
Второй тест проверяет как создается полноценный пользователь, под которым после можно залогиниться
Дело за модульным тестом
Так же два теста, на тех же два случая
Для UserService у меня так же есть парочка тестов. Один, самый простой, мы реализовали а вот остальные, чтобы не мозолили мне мое ОЗУ, я вынес в виде комментариев.
В логинку так же добавлялся новый код - проверим это
Странно, но почему-то поломался функиональный тест админки.
Ах ну да, я допустил ошибку. тест раньше запускался со своим напарником, который приводил к страничке добавления нового пользователя, а потом наш тест делал свое дело. Теперь тест запустили отдельно и он не увидав своей странички добавления пользователей слетел. Лечится просто
Теперь стоит проверить, что незалогиненный пользователь не имеет права доступа к админке
Помнится я когда-то это проверял в контроллере ExamController.
Скопипастим (походу оставляя пометку где-то, что дублирование надо устранить) оттуда
Раз клиентов у метода isUserLoggedOut теперь два и находятся они в разных местах, то новое место надо искать и для этого метода. Вырезаем его
И переносим в контроллер логинку, а так же делаем статическим и публичным (не нравится мне это, но пока это единственное что пришло в голову и делается быстро)
Меняем в клиенте адресат
Вуаля - тест прошел
Но поломалось другое
Анализируем поломку
Непонятно. А второй тест?
Непонятно - включаем debug...
Идем глубже
Ну вот, в сессии нет атрибута с именем user_name
Все дело в том, что при залогинивании как админ мы не сохраняем в сессии этого админа, а потому проверка на то, а не залогинен ли пользователь, будет всегда возвращать false.
По этой причине немного переделаем контроллер логинки и тест заработает
В скриншоте голубенькие линии говорят, что строчка быладо изменения в классе, но поменял свое местоположение. Зелененькие линии - код не менялся. Красные линии - код был добавлен.
Идем дальше - модульные тесты естественно полетят
А потому мы еще немного пошаманим - так логичнее, чтоли
Походу я хочу инкапсулировать в UserService логику проверки пользователь админ или нет.
Новый метод в интерфейсе
Новая пустышка в реализации
Ну и в соке, блин. Может удалить его?
Наполняем смыслом пустышку (код раньше был в контроллере)
Снова что-то моломалось
Получается я ожидаю что админ залогинился, а его отправили на страничку с сообщением что он неправильно что-то.
В коде это говорит о том, что checkUser с аргументами admin admin возвращает false
Таки да. Исправим это недоразумение
Тест заработал.
Теперь я хочу немного порефакторить код тестов. Для начала трехэтапный экстракт
Вводим поясняющую константу
Тут же добавляем ее брата
Во всем наборе тестов теперь можно избавиться от лишних строк.
Еще две константы
Расширим метод добавлением нового аргумента
Банальное переименовывание
И немного доработки. Три теста работают
Что же с другим?
Ожидаетсяпереход на админку, но этого не происходит. Почему? Да потому что моки не настроены правильно.
Сделаю как я еще в одном месте выделение метода
Этот метод нам понадобится тут
Тест зеленый. Inline и готово!
Продолжаем выделение методов
Я немного подумал и решил, что выделять стоило две строчки а не одну. Перемещу, пока не поздно
Теперь можно сократить код
А главное тесты зеленые
Так, что у нас с unit тестами админ контроллера?
Ожидается не то, что есть на самом деле, а потому немного extract method
Я уже созрел для выделения части общих методов юнит тестов в абстрактный класс
Пускай это будет ControllerTestm, и перемести мы туда все, что касается контролирования в сервлетах
Тесты зеленые. Замечательно!
Вот как выглядит класс, от которого я собираюсь наследовать все наборы тестов для тестирования контроллеров
Сделаем это для тестов логин контроллера
Немного доработки, ибо расширяем
Порядок так же стоит навести и в самом родителе. Раз он абстрактный, то ничего не должен знать про конкретную версию контроллера. так же модификаторы доступа сделал всем мокам protected, дабы наследники могли видеть их как свои
Основная причина ошибок - мок добавили, а проинитить ипровалидировать его забыли.
Чистим тесты для админ контроллера
Вот теперь самое интересное. Метод приватный, а значит надо либо менять модификатор доступа (что ради тестов не очень хорошо)
Либо использовать рефлексию java
Теперь надо во всех тестах прокинуть все эксцепшены вверх. Один раз сделаю, и больше не буду париться. Можно было написать try/catch с оборачиванием всех checked Exception в unchecked RuntimeException, но я не стал этого делать.
Почему-то NPE. Почему?
Снова дебажим
controller is null. Почему?
Добавлю ассерт, чтобы информативнее было на будущее
Ах да, я забыл проинитить контроллер и вставить в него все, что надо. Но почему-то вставка userService не удалась
Все потому что copy past - я тупо не то инджектил
Пошли дальше. Тест админ контроллера
Можем легко переписать с использованием новых методов родителя
Итого все тесты зеленые
Теперь можно дописать все тесты, которые я оставил в виде комментариев раньше
Добавим новый тест - простой пользователь не имеет права доступа к админке.
Добавление валидации в контроллер привело к озеленению нового теста, но поломало старый
Первое, что я провтыкал - это то, что пользователь хранится в сессии а не в риквесте.
Посуди сам. Логин контроллер делает всю небходимую валидацию и его казалось бы достаточно, если бы не хитрый залогиненный пользователь, который вдруг попробует перейти на админку - для этого уже на админке потребуется проверить а не адмил ли это, и если это так, то отправить снова на логинку. Поэтому удаляем лишний параметр везде, где надо
Естественно это повлияет на изменение интерфейсной части
и всех тех мест, где этот интерфейс используется
Это даже приведет к удалению неактуальных тестов
и изменению мока userService
Изменится и основная реализация
После того, как все ошибки компилятора будут улажены можно запустить тесты с надеждой что все будет хорошо
А нет! Гипотезы у меня нет, а потому включаем debug
Снова я ошибся с именами параметров сессии?
Да.
Исправление повлечет за собой озеленение пачки тестов, но некоторые все же останутся нерабочими
А все дело в том, что мы используем пользователя sasha для тестирования админки. Непорядок!
Исправление помогло.
Немного поэкстрактим
И переместим новый метод в родителя. Это метод - помощник при тестировании контроллеров - там ему и место
Два новых модульных теста для админки. Один сразу работает, а другой нет
Моки придется перепрограммировать
Естественно вслед за этим последует extract нового метода
Теперь можно использовать новый метод во всех местах
Я так же забыл переместить константы в родителя
Запустим-ка все тесты?
Зеленые? Коммитимся!
Это был большой кусочек, но зато теперь у нас (вернее у того кто знает админский пароль) есть возможность добавлять новых пользователей.
Продолжение следует...
Комментариев нет:
Отправить комментарий