Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /*
- в следующую версию - включи доработанный rest-server.py
- */
- ****************************
- public class ConciseAPI {
- /*
- Получилась - помесь класса = контейнера универсальных вспомогательных методоа
- и класса-ресусра (аналогия с пейджами) = класса - контейнера вспомогательных инструментов для работы с рест-сервером
- Такие инструменты - разумно вынести в аналог пейджа.
- Теперь у нас будет пекедж не pages, a resources.
- А правила работы с таким классом - аналогичные.
- Поскольку мы работаем только с тасками - в имена методов-действий можно не включать слово Tasks
- Имена методов-проверок - по-прежнему указываем точно
- Логично было бы назвать класс Tasks (будем реализовывать как ресурс-модуль, а не ресурс-объект)
- И в коде потом можно без статического импорта писать
- Response response = Tasks.create(URI, "give lesson");
- Это было бы и наглядно, и на будущее - на случай, если в тесте появятся запросы к другим ресурсам
- Tasks - неплохое имя
- но у нас есть класс Task
- и можно сказать - что мы используем один термин для достаточно разных понятий….
- Можно в таком случае для класса-ресурса использовать TasksApi или TasksRest в таком случае…
- Но и Tasks - тоже неплохо…, но хуже)
- Можно пойти на эту уступку в принципе - код понятный, названия наглядные
- На реальном проекте обычно создают класс-ресурс (объект или модуль) не сразу, когда начали писать тесты.
- А позже - когда появится необходимость переиспользовать вспомогательные методы в разных тест-классах.
- Или если одни более квалифицированные члены команды разрабатывают класс-ресурс,
- а другие - используют для разработки тестов. Но или если так принято в данной команде)
- Т е - это делается обычно при необходимости. С пейджами - в общем аналогично.
- */
- *****************************
- public class Task {
- private String uri = "http://localhost:5000/todo/api/v1.0/tasks/"
- /*
- Не надо внутрь класса-контейнера таски - встраивать урл
- что передадут в конструктор в качестве урла - такой у таски урл и будет
- не нужны тут эти технические подробности
- это - данные не контейнера
- это - данные о
- где у нас задеплоен сервер = http://localhost:5000/ = настраиваемая часть (ведь это может быть по-разному)
- и собственно - данные нашего рест-сервера - что вот такой урд нужен для запросов - .../todo/api/v1.0/tasks/
- Предлагаю в Сonfiguration.baseUrl - сохранять домен
- чтобы его можно было от случая к случаю реконфигурить...
- Ведь это как раз та часть урла - которая не принадлежит ресурсу,
- и зависит от того на каком сервере/по-какому-адресу ресурс/веб-сервис задеплоен
- На самом деле здесь есть несколько способов как лучше сделать
- Первый, если ты уверен что тебе не нужно будет одновременно/параллельно посылать запросы на разные урлы
- То тогда можно использувать Configuration.baseUrl типа обычныя глобальная переменная (публичное статическое поле класса)
- если нет, тогда лучше "ооп" подход, когда обьект-ресурс помнит свой базовый урл (доменную часть)
- который передается при создании через конструктор
- */
- *****************************
- src / test / java / api / core / BaseTest.java
- public class BaseTest {
- /*
- располагать предка тест-класса в пекедже core
- который еще и в ветке src / test находится - таки перебор
- ниже приведу пример структуры проекта
- я думаю, что в данном случае - существование предка у тест-класса - не оправдано
- ниже поясню
- */
- *************************************************
- public static Task defaultTask1 = new Task("First Default item", false, "Title first item", 1);
- public static Task defaultTask2 = new Task("Second Default item", false, "Title second item", 2);
- /*
- а как тебе вот такой вариант?
- */
- public final Task[] DEFAULT_TASKS = {
- new Task("Buy groceries", "Milk, Cheese, Pizza, Fruit, Tylenol", false, TasksApi.uri + "/1"),
- new Task("Learn Python", "Need to find a good Python tutorial on the web", false, TasksApi.uri + "/2")
- };
- /*
- обрати внимание - я объявила это поле - как константу
- https://google.github.io/styleguide/javaguide.html#s5.2.4-constant-names
- чтоб полчеркнуть - природу этих данных
- и от объявления данных как массива - тоже получим дополнительные преимущества(ниже рассмотрим)
- С таким вынесением тестовых данных в переменные надо быть осторожным
- Важно - не надо тестовые данные объявлять в ресурс модуле (опять аналогия с пейджом - мы и там такого правила придерживались)
- Это совсем грубая ошибка
- Также - не стоит это прятать внутри предка тест-класса
- Это должно быть видно в тест-классе - явно
- Да и к логике предка-тест-класса - это явно не относится (гугли Single Responsibility Principle)
- Или - если тест-классов у тебя несколько
- используй отдельный класс для тестовых данных (мы такой прием уже использовали для тестов GMail)
- В данном задании - правда есть смысл объявить эти данные
- Часто тестовые данные разумно оставлять в логике самих тестов - таким образом код тестов более очевидный. Пример - тудуэмвиси тесты
- Но - если эти данные вынесены в переменные - то их и в таких случаях стоит использовать и уже нигде не дублировать в коде
- Это у тебя как раз ОК
- */
- ***********************************************************
- @BeforeClass
- public static void startConditions(){
- ...
- @AfterClass
- public static void clearData() {
- /*
- добавить таски до теста и удалить после теста - не самое удачное решение)
- да и даже такое решение - требует доработки rest-server.py
- так что не забудь его привести как часть решения
- причины просты
- вот например ты тестишь добавление таски
- и в предварительных действиях - используешь - ...добавление таски)
- получится - что мы и не узнаем - что у нас есть проблемы с добавлением)
- еще вариант
- тестим редактирование таски
- оно работает
- а вот добавление - допустим, нет
- и мы опять не узнаем - что как работает
- короче говоря - было бы полезнее иметь метод на уровне rest-server.py, который
- будет возвращать состяние списка тасок в исходное состояние
- вот какие у нас есть варианты
- условно назовем вариант delete all
- (тобой реализованный вариант)
- когда предварительные действия в тест-метода = delete all + add some tasks
- и вариант reset = когда при его вызове - снова получаем те же 2 таски, что были на начало теста...
- Delete all
- В такой реализации - код удаления всех тасок без каких-то сложностей особенных.
- Это плюс. А минус в том, что для гивен-методов тогда надо реализовывать дополнительное что-то.
- На начало тест-метода часто нужны существующие таски - нам же надо реализовать тесты для апдейта или удаления.
- Судя по реализации гивенов - когда в гивенах создаются таски - то используется сначала delete all,
- а потом добавление тасок.
- Т е действий - больше, чем если бы мы реализовали reset.
- Если deleteAll or add отвалились - то гивены не работают.
- Хотя, с другой стороны, если reset отвалился - тоже беда)))
- Но, в таком случае, все же это меньшее зло,
- т к этот reset служит только для тестовых целей
- и с развитием сервера он вряд ли будет меняться,
- да и логика у него и у так реализованных гивенов - значительно проще...
- Более того, с reset получше вероятности по надежности гивенов -
- с reset только одно звено (примитивное),
- а с delete all - два (реально использующихся в рабочих целях = более сложных = add + delete all)
- Еще такой момент.
- При тестировании операции add - вызывать гивен, в котором уже вызван add - как-то странно)
- Т е при таких раскладах - надо вызывать гивен строго в ситуации, кгда тасок мы не добавляем
- Это конечно не KISS тоже - то, что надо помнить о каких-то ограничениях
- Из этой же серии
- delete all - тоже операция, которая может быть востребованой в работе приложения
- Значит - и ее надо тестировать)
- А раз так - для delete all вызываем гивен, в котором вызываем delete all
- Тут уже идея с delete all в качестве вспомогательной операции - заходит в тупик)
- С Delete all хорошо то, что гивены гибкие и можно более интересную тестовую ситуацию создать.
- правда, вопрос - кому она тут нужна)))
- т к тут, на этом этапе - достаточно простые вещи нам нужно проверить
- супер-навороченых гивенов в общем-то и не надо
- вывод - = с reset = 2 таски по дефолту более KISS получается)
- если бы стояла задача в REST API тестах прогнать работу с тасками -
- у которых тексты разной длины/кодировки ....,
- то может - навороченные гивены, использующие delete all, были бы и нужны.
- да и то - не факт )
- И вот это очень серьезное замечание - когда delete all тестируется используя в предварительніх действиях саму себя
- Я - сторонник варианта с reset - как варианта, в котором меньшее количество вещей может сломаться
- и как более быстрого и более однозначного варианта
- */
- /*
- Теперь по реализации ресета
- Нам необходима возможность получить состояние на начало работы = когда у нас были только 2 предопределенные таски
- Нам нужно реализовать в rest-server.py возможность для очистки базы
- У нас же REST сервис, потому нужно следовать “идеологии REST”
- А она говорит - что можно использовать только специальные http methods
- И только по определенному принципу
- Например,
- для создания - использовать post,
- для апдейта - метод put
- и т д
- И нам важно соблюсти этот принцип и свести написание сервисов к одному шаблону
- Вся идея REST была в том, чтобы чтобы потом все могли “общаться на одном языке” и таким образом “понимать друг друга”
- можно было бы реализовать что-то типа такого
- @app.route('/todo/api/v1.0/tasks/reset', methods = [‘PUT’]) (edited)
- но, хотя мы и делаем “апдейт"
- но - апдейт не ресурса...
- т к reset - это не ресурс
- вот если бы мы для /tasks посылали PUT, то было бы все честно
- а /tasks/reset - это уже не ресурс
- это просто “хак-урл” , чтобы нам выполнить определенное нужное нам действие
- потому в данном случае абсолютно нормально - посылать GET
- наверное, можно и PUT… решай самостоятельно
- еще довод за GET
- обрати внимание - все остальные запросы что-то возвращают - делают return …
- это логично - клиент послал запрос, нужно ему что-то вернуть)
- и тут было бы логично вернуть все таски = (то же, что возвращает запрос get)
- это важный довод - почему стоит использовать именно GET для нашего запроса
- Не надо в данном методе сервиса - дублировать коллекцию tasks
- Нужно грамотно воспользоваться тем, что уже есть в коде
- Если начальное состояние фиксировать в некой коллекции tasks_default, при старте и ресете - копировать в tasks содержимое tasks_default
- В python есть способ - скопировать содержимое одного списка в другой с помощью одной операции, а не переносить каждый элемент списка отдельно
- Полезные линки
- http://www.python-course.eu/python3_global_vs_local_variables.php
- https://docs.python.org/2/library/copy.html
- http://joxi.ru/Dr8vxzPFk3Kl32
- http://stackoverflow.com/questions/2612802/how-to-clone-or-copy-a-list-in-python
- http://joxi.ru/J2b9KdBt41qEpm
- */
- /*
- В результате таких преобразований - останется использовать лишь предварительые действия (After-действий - уже не будет)
- смотри - этот гивен - это часть тест логики
- как минимум нельзя называть такой базовый тест класc как BaseTest
- потому что имя ничего не говорит и скрывает "важное"
- но более того
- этот гивен на самом деле такой важный, что лучше вообще не скрывать его
- ни в каком базовом классе....
- его желательно "видеть" в самом тесте
- часто даже не стоит такие вещи в @Before выносить, не смотря на то что код будет не DRY
- но будет еще более явным и очевидным)
- Хотя в этом случае наверное с @Before будет таки лучше )
- а в классы с именем BaseTest - желательно выносить что-то такое что однозначно общее для всех тестов,
- какие-то настройки может конфигурационные, которые в контексте тест логики смысла не имеют
- и их не грех "скрыть с глаз долой"
- Вот и получаем - что от предка тест-класса - избавились)
- */
- ***********************************************
- public class ConciseAPI {
- public static Invocation.Builder requestTo(String uri) {
- public static Invocation.Builder authorized(Invocation.Builder requestBuilder){
- /*
- вот эти методы - универсальные (если "miguel:python" - вынести как параметр метода, а не зашивать это внутри)
- их лучше реализовать в отдельном классе, например RestHelpers
- а уже в классе-ресурсе - реализуешь authorizedRequest(String uri)
- в котором вызывай универсальный метод и указывай "miguel:python"
- в данном случае - "miguel:python" - это не тестовые данные -
- т к только так и можно авторизироваться
- если бы были варианты - то уж работали бы - как с обычними тестовыми данными
- класс-ресурс тогда ничего про них не должен знать(напрямую - не обращаться)
- а получать такое - через параметры, то ли методов, то ли скоего конструктора - если речь о ресурсе-объекте
- */
- *****************************************************
- public static void createTask(Task newTask){
- /*
- Соглашусь, разумно сразу в методе-действии - проверять респонс
- т к вариантов тут нету - эти проверки не зависят от тестового контекста
- Правильный статус ответа на каждый из запросов - строго определенный
- И если мы будем проверять статут ответа в тест-методе - то надо об этом думать и вспоминать - какой статус в каких случаях верный
- И можно выкрутиться - проверять статус ответа в нутри степа, а возвращать собственно entity из запроса.
- Сразу пара потенциально полезных моментов)
- Если встроить проверку response кода прямо в сам степ
- Тут сразу появляется интересный нюанс
- Дело в том, что мы так спрячем тестовую логику внутрь степа, что не очень хорошо
- С другой стороны - эта проверка = само собой разумеющееся
- Эта проверка достаточно стандартная и всегда будет повторяться после вызова степа (если будет жить вне степа)
- А раз так - то почему бы и не спрятать?
- На самом деле - нет однозначного ответа на этот вопрос...
- Так - со спрятанной проверкой - код станет проще во всех тестах - но втанет до некоторой степени “магическим”
- Тут надо решать самостоятельно - как лучше )
- На твое усмотрение
- Еще момент
- В этих API тестах - самих тестов может быть не так уж и много -
- чтоб еще и заморачиваться над DRY кода...нужно и этот аспект учитывать
- Но если пойти этим путем = прятать в степе проверку статуса response, то код будет намного более удобным
- Собственно, ты так и сделал)
- Эти пояснения - для понимания - какие есть варианты
- Насчет имени параметра
- Я бы использовала просто task
- ведь ясно - что только новую таску можно создать )
- */
- *************************
- public static List<Task> getListTasks(){
- /*
- я бы просто метод назвала - get
- с учетом имени ресурса-модуля - будет вполне ок
- */
- *************************************
- public static void assertTasks(Task ...tasks)
- /*
- реализовал оптимально)
- единственное - форматирование кода подправь
- и отступы между методами и между блоками кода - тоже единообразнее сделай(это автоматом при реформатировании не поправляется)
- как реформатировать = выделить код + в меню Code->reformat code
- */
- ************************************
- public static void updateTask(int id, Task updatedTask){
- /*
- id - передавать не надо
- у updatedTask (кстати, подусай над именем параметра) - есть uri
- этого достаточно
- просто передавай как параметр - таску с верным uri
- и все ок будет
- Task task = new Task("t2 title", "t2 description", true, TasksApi.uri + "/2");
- TasksApi.update(task);
- тут - унас нету задачи - получить все таски, найти нужную по uri
- и для нее вызвать update
- мы уже знаем - какую таску мы обновляем
- мы уже рассчитвываем на какое-то начальное состояние
- потому- тут - реализуй только update конкретной таски
- а потом - просто проверть результаты
- кстати - поступай единообразно
- если решил проверять результаты респонса внутри степ-методов - то так и дальше делай
- а в самом тесте - проверишь тестовую ситуацию = состояние всего списка тасок
- вот это - не скрывай в степ-методе точно
- */
- ************************************************
- public static void deleteTask(int id){
- /*
- Тут действительно проще оперировать id
- только тоже - не нужно никаких получений списков и их обходов
- достаточно - собрать uri - по ид-шнику
- все данные у тебя есть
- ну и проверь респонс
- */
- ******************************
- public class RestfullTasksTest extends BaseTest {
- /*
- Не забывай о правилах CamelCase
- https://google.github.io/styleguide/javaguide.html#s5.3-camel-case
- название - RestFulTasksTest - будет ок
- посмотри на написание термина RESTful
- https://en.wikipedia.org/wiki/Representational_state_transfer
- */
- *********************************
- public static final String URI = "http://localhost:5000/todo/api/v1.0/tasks";
- /*
- про это выше написано - как разбить и где расположить
- uri - нужен всем степ-методам и тест-методам
- т е - он тоже инструмент ресурс-модуля
- который еще и настраивается через Configuration.baseUri
- */
- *******************************************
- assertEquals("Unauthorized access", response.readEntity(ErrorContainer.class).getError());
- /*
- раз так реализована проверка текста ошибки
- тогда на уровне класса ErrorContainer - тебе не нужы методы equals & hashCode
- */
- ******************************************
- @Test
- public void testReadTasks(){
- assertTasks(defaultTask1, defaultTask2);
- }
- /*
- технически - так и есть
- делаем гет и проверку
- но тут - в testRead - получаем сокрытие тестовой логики
- тестовое действие = гет
- проверка - assertEquals(...) - сравни ожидаемые результаты и результатами из респонса
- правильно = когда каждое тетсовое действие и каждая проверка - отражались в тест-методе
- Иначе - не ясно из кода тест-метода - что ж мы таки тут делаем
- да, мы иногда позволяем себе заворачивать проверки - в метод-действие и наоборот
- но - это исключения из общего правила
- и мы это делаем всегда - зачем-то или почему-то
- бездумно и бесцельно - не скрывай в шагах проверки и в проверках шаги
- а вот в остальных тест-методах - использование метода assertTasks - как раз уместно
- мы скрыли технические детали проверки
- а тут - используя assertTasks - мы скрыли - тестовую логику
- важно эту разницу понимать
- */
- **********************
- public void testCreate(){
- /*
- реализуй 2 варианта теста - создание таски, только по ее title
- и создание таски по полной информации
- для остальных операций - достаточно одного теста (1 операция = 1 тест, ну и один е2е)
- */
- ********************************************
- @Test
- public void testCreateUpdateDelete(){
- Task newTask = new Task("New task item", false, "Title new task", 3);
- Task updateTask = new Task("Upd task", false, "Some title", 3);
- //create
- createTask(newTask);
- //update
- updateTask(3, updateTask);
- //delete
- deleteTask(1);
- assertTasks(defaultTask2, updateTask);
- }
- /*
- советую объявлять и инициализировать Task newTask и Task updateTask - непосредственно перео выполнением операции
- а не в начале метода
- цель - чтоб не бегать глазами по коду
- где нужно - там и видим
- не забывай - каждая операция в е2е - должна быть проверена)
- и проверка должна касаться всего списка тасок, а не одной таски
- т к возможно - после выполнения операции над одной таской
- состояние других - исказилось
- подумай - что дают комментарии
- я бы удалила третью таску
- т к в фиче-тесте - мы уже удаляли таску - созданную по умолчанию
- а тут - удалим таску, созданную вручную
- (это станет важно - т к начальное состояние - будет задаваться спец запросом)
- */
- ****************************************************
- /*
- http://joxi.ru/nAyqEx7HXvxQoA
- вот пример хорошей структуры проекта
- в src \ main
- core - универсальное, что можно переиспользовать в разных проектах
- pages - пейджи тоже можно переиспользовать для других тестов этого же приложения
- в src \ test
- testdata - тестовые данные (если такие есть и они вынесены в отдельный класс)
- testconfigs - предки тест-класса (так можно их изолировать от собственно тест-классов - чтоб легче было ориентироваться
- про пекеджи еще немного)
- если GroupID = com.somesite
- а проект todomvctest
- то пакет корневой должен быть com.somesite.todomvctest
- логика - чтобы "не смешивались имена сущностей"
- внутри одной компании - может быть несколько проектов)
- и у всех у них один com.somesite - базовый пекедж
- но для каждого проекта должен быть свой “базовый пекедж проекта"
- иначе все смешается)
- важно то, что когда этот проект выльется в отдельную библиотеку,
- то не будет конфликтов при его подключении
- */
Advertisement
Add Comment
Please, Sign In to add comment