julia_v_iluhina

Untitled

Dec 16th, 2016
105
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Java 33.17 KB | None | 0 0
  1. /*
  2.     в следующую версию - включи доработанный rest-server.py
  3. */
  4. ****************************
  5.  
  6. public class ConciseAPI {
  7. /*
  8.     Получилась - помесь класса = контейнера универсальных вспомогательных методоа
  9.     и класса-ресусра (аналогия с пейджами) = класса - контейнера вспомогательных инструментов для работы с рест-сервером
  10.  
  11.     Такие инструменты - разумно вынести в аналог пейджа.
  12.     Теперь у нас будет пекедж не pages, a resources.
  13.     А правила работы с таким классом - аналогичные.
  14.  
  15.     Поскольку мы работаем только с тасками - в имена методов-действий можно не включать слово Tasks
  16.     Имена методов-проверок - по-прежнему указываем точно
  17.  
  18.     Логично было бы назвать класс Tasks (будем реализовывать как ресурс-модуль, а не ресурс-объект)
  19.     И в коде потом можно без статического импорта писать
  20.             Response response = Tasks.create(URI, "give lesson");
  21.  
  22.  
  23.     Это было бы и наглядно, и на будущее - на случай, если в тесте появятся запросы к другим ресурсам
  24.  
  25.     Tasks - неплохое имя
  26.     но у нас есть класс Task
  27.     и можно сказать - что мы используем один термин для достаточно разных понятий….
  28.  
  29.     Можно в таком случае для класса-ресурса использовать TasksApi или  TasksRest в таком случае…
  30.  
  31.     Но и Tasks - тоже неплохо…, но хуже)
  32.     Можно пойти на эту уступку в принципе - код понятный, названия наглядные
  33.  
  34.  
  35.     На реальном проекте обычно создают класс-ресурс (объект или модуль) не сразу, когда начали писать тесты.
  36.     А позже - когда появится необходимость переиспользовать вспомогательные методы в разных тест-классах.
  37.     Или если одни более квалифицированные члены команды разрабатывают класс-ресурс,
  38.     а другие - используют для разработки тестов. Но или если так принято в данной команде)
  39.     Т е - это делается обычно при необходимости. С пейджами - в общем аналогично.
  40. */
  41. *****************************
  42.  
  43. public class Task {
  44.     private String uri = "http://localhost:5000/todo/api/v1.0/tasks/"
  45. /*
  46.     Не надо внутрь класса-контейнера таски - встраивать урл
  47.     что передадут в конструктор в качестве урла - такой у таски урл и будет
  48.  
  49.     не нужны тут эти технические подробности
  50.     это - данные не контейнера
  51.     это - данные о
  52.         где у нас задеплоен сервер = http://localhost:5000/ = настраиваемая часть (ведь это может быть по-разному)
  53.         и собственно - данные нашего рест-сервера - что вот такой урд нужен для запросов - .../todo/api/v1.0/tasks/
  54.  
  55.     Предлагаю в Сonfiguration.baseUrl - сохранять домен
  56.     чтобы его можно было от случая к случаю реконфигурить...
  57.     Ведь это как раз та часть урла - которая не принадлежит ресурсу,
  58.     и зависит от того на каком сервере/по-какому-адресу ресурс/веб-сервис задеплоен
  59.  
  60.     На самом деле здесь есть несколько способов как лучше сделать
  61.  
  62.     Первый, если ты уверен что тебе не нужно будет одновременно/параллельно посылать запросы на разные урлы
  63.     То тогда можно использувать Configuration.baseUrl типа обычныя глобальная переменная (публичное статическое поле класса)
  64.  
  65.     если нет, тогда лучше "ооп" подход, когда обьект-ресурс помнит свой базовый урл (доменную часть)
  66.     который передается при создании через конструктор
  67.  
  68. */
  69. *****************************
  70.  src / test / java / api / core / BaseTest.java
  71.  
  72. public class BaseTest {
  73. /*
  74.     располагать предка тест-класса в пекедже core
  75.     который еще и в ветке src / test находится - таки перебор
  76.  
  77.     ниже приведу пример структуры проекта
  78.  
  79.     я думаю, что в данном случае - существование предка у тест-класса - не оправдано
  80.     ниже поясню
  81. */
  82. *************************************************
  83.     public static Task defaultTask1 = new Task("First Default item", false, "Title first item", 1);
  84.     public static Task defaultTask2 = new Task("Second Default item", false, "Title second item", 2);
  85.     /*
  86.         а как тебе вот такой вариант?
  87.     */
  88.     public final Task[] DEFAULT_TASKS = {
  89.             new Task("Buy groceries", "Milk, Cheese, Pizza, Fruit, Tylenol", false, TasksApi.uri + "/1"),
  90.             new Task("Learn Python", "Need to find a good Python tutorial on the web", false, TasksApi.uri + "/2")
  91.     };
  92. /*
  93.     обрати внимание - я объявила это поле - как константу
  94.     https://google.github.io/styleguide/javaguide.html#s5.2.4-constant-names
  95.  
  96.     чтоб полчеркнуть - природу этих данных
  97.     и от объявления данных как массива - тоже получим дополнительные преимущества(ниже рассмотрим)
  98.  
  99.     С таким вынесением тестовых данных в переменные надо быть осторожным
  100.  
  101.     Важно - не надо тестовые данные объявлять в ресурс модуле (опять аналогия с пейджом - мы и там такого правила придерживались)
  102.     Это совсем грубая ошибка
  103.  
  104.     Также - не стоит это прятать внутри предка тест-класса
  105.     Это должно быть видно в тест-классе - явно
  106.     Да и к логике предка-тест-класса - это явно не относится (гугли Single Responsibility Principle)
  107.  
  108.     Или - если тест-классов у тебя несколько
  109.     используй отдельный класс для тестовых данных (мы такой прием уже использовали для тестов GMail)
  110.  
  111.     В данном задании - правда есть смысл объявить эти данные
  112.     Часто тестовые данные разумно оставлять в логике самих тестов - таким образом код тестов более очевидный. Пример - тудуэмвиси тесты
  113.     Но - если эти данные вынесены в переменные - то их и в таких случаях стоит использовать и уже нигде не дублировать в коде
  114.     Это у тебя как раз ОК
  115. */
  116. ***********************************************************
  117.     @BeforeClass
  118.     public static void startConditions(){
  119. ...
  120.     @AfterClass
  121.     public static  void clearData() {
  122. /*
  123.     добавить таски до теста и удалить после теста - не самое удачное решение)
  124.  
  125.     да и даже такое решение - требует доработки  rest-server.py
  126.     так что не забудь его привести как часть решения
  127.  
  128.     причины просты
  129.     вот например ты тестишь добавление таски
  130.     и в предварительных действиях - используешь - ...добавление таски)
  131.     получится - что мы и не узнаем - что у нас есть проблемы с добавлением)
  132.  
  133.  
  134.     еще вариант
  135.     тестим редактирование таски
  136.     оно работает
  137.     а вот добавление - допустим, нет
  138.     и мы опять не узнаем - что как работает
  139.  
  140.     короче говоря - было бы полезнее иметь метод на уровне rest-server.py, который
  141.     будет возвращать состяние списка тасок в исходное состояние
  142.  
  143.     вот какие у нас есть варианты
  144.  
  145.     условно назовем вариант delete all
  146.     (тобой реализованный вариант)
  147.     когда предварительные действия в тест-метода =  delete all + add some tasks
  148.  
  149.     и вариант reset = когда при его вызове - снова получаем те же 2 таски, что были на начало теста...
  150.  
  151.     Delete all
  152.     В такой реализации - код удаления всех тасок без каких-то сложностей особенных.
  153.     Это плюс. А минус в том, что для гивен-методов тогда надо реализовывать дополнительное что-то.
  154.     На начало тест-метода часто нужны существующие таски - нам же надо реализовать тесты для апдейта или удаления.
  155.  
  156.     Судя по реализации гивенов - когда в гивенах создаются таски -  то используется сначала delete all,
  157.     а потом добавление тасок.
  158.     Т е действий - больше, чем если бы мы реализовали reset.
  159.  
  160.     Если deleteAll or  add отвалились - то гивены не работают.
  161.     Хотя, с другой стороны, если reset отвалился - тоже беда)))
  162.     Но, в таком случае, все же это меньшее зло,
  163.     т к этот reset служит только для тестовых целей
  164.     и с развитием сервера он вряд ли будет меняться,
  165.     да и логика у него и у так реализованных гивенов - значительно проще...
  166.  
  167.     Более того, с reset получше вероятности по надежности гивенов -
  168.     с reset только одно звено (примитивное),
  169.     а с delete all - два (реально использующихся в рабочих целях = более сложных = add + delete all)
  170.  
  171.     Еще такой момент.
  172.     При тестировании операции add - вызывать гивен, в котором уже вызван add - как-то странно)
  173.     Т е при таких раскладах - надо вызывать гивен строго в ситуации, кгда тасок мы не добавляем
  174.     Это конечно не KISS тоже - то, что надо помнить о каких-то ограничениях
  175.  
  176.     Из этой же серии
  177.     delete all - тоже операция, которая может быть востребованой в работе приложения
  178.     Значит - и ее надо тестировать)
  179.     А раз так - для delete all вызываем гивен, в котором вызываем delete all
  180.     Тут уже идея с delete all в качестве вспомогательной операции - заходит в тупик)
  181.  
  182.     С Delete all хорошо то, что гивены гибкие и можно более интересную тестовую ситуацию создать.
  183.     правда, вопрос - кому она тут нужна)))
  184.     т к тут, на этом этапе - достаточно простые вещи нам нужно проверить
  185.     супер-навороченых гивенов в общем-то и не надо
  186.  
  187.  
  188.     вывод -  =  с reset = 2 таски по дефолту более KISS получается)
  189.     если бы стояла задача в REST API тестах прогнать работу с тасками  -
  190.     у которых тексты разной длины/кодировки ....,
  191.     то может - навороченные гивены, использующие delete all, были бы и нужны.
  192.     да и то - не факт )
  193.     И вот это очень серьезное замечание - когда delete all тестируется используя в предварительніх действиях саму себя
  194.     Я - сторонник варианта с reset - как варианта, в котором меньшее количество вещей может сломаться
  195.     и как более быстрого и более однозначного варианта
  196.  
  197. */
  198.  
  199. /*
  200.     Теперь по реализации ресета
  201.     Нам необходима возможность получить состояние на начало работы = когда у нас были только 2 предопределенные таски
  202.  
  203.         Нам нужно реализовать в rest-server.py возможность для очистки базы
  204.  
  205.         У нас же REST сервис, потому нужно следовать “идеологии REST”
  206.  
  207.         А она говорит - что можно использовать только специальные http methods
  208.         И только по определенному принципу
  209.         Например,
  210.         для создания - использовать post,
  211.         для апдейта - метод put
  212.         и т д
  213.  
  214.         И нам важно соблюсти этот принцип и свести написание сервисов к одному шаблону
  215.         Вся идея REST была в том, чтобы чтобы потом все могли “общаться на одном языке” и таким образом “понимать друг друга”
  216.  
  217.         можно было бы реализовать что-то типа такого
  218.         @app.route('/todo/api/v1.0/tasks/reset', methods = [‘PUT’]) (edited)
  219.  
  220.  
  221.         но, хотя мы и делаем “апдейт"
  222.         но - апдейт не ресурса...
  223.         т к reset - это не ресурс
  224.  
  225.         вот если бы мы для  /tasks посылали PUT, то было бы все честно
  226.         а /tasks/reset - это уже не ресурс
  227.         это просто “хак-урл” , чтобы нам выполнить определенное нужное нам действие
  228.         потому в данном случае абсолютно нормально - посылать GET
  229.         наверное, можно и PUT… решай самостоятельно
  230.  
  231.         еще довод за GET
  232.         обрати внимание - все остальные запросы что-то возвращают - делают return …
  233.         это логично - клиент послал запрос, нужно ему что-то вернуть)
  234.         и тут было бы логично вернуть все таски = (то же, что возвращает запрос get)
  235.         это важный довод - почему стоит использовать именно GET для нашего запроса
  236.  
  237.      Не надо в данном методе сервиса - дублировать коллекцию tasks
  238.      Нужно грамотно воспользоваться тем, что уже есть в коде
  239.      Если начальное состояние фиксировать в некой коллекции tasks_default, при старте и ресете - копировать в  tasks содержимое tasks_default
  240.      В python есть способ  - скопировать содержимое одного списка в другой с помощью одной операции, а не переносить каждый элемент списка отдельно
  241.  
  242.      Полезные линки
  243.      http://www.python-course.eu/python3_global_vs_local_variables.php
  244.      https://docs.python.org/2/library/copy.html
  245.      http://joxi.ru/Dr8vxzPFk3Kl32
  246.      http://stackoverflow.com/questions/2612802/how-to-clone-or-copy-a-list-in-python
  247.      http://joxi.ru/J2b9KdBt41qEpm
  248.  
  249.  
  250. */
  251.  
  252. /*
  253.     В результате таких преобразований - останется использовать лишь предварительые действия (After-действий - уже не будет)
  254.  
  255.     смотри - этот гивен - это часть тест логики
  256.     как минимум нельзя называть такой базовый тест класc как BaseTest
  257.     потому что имя ничего не говорит и скрывает "важное"
  258.  
  259.     но более того
  260.     этот гивен на самом деле такой важный, что лучше вообще не скрывать его
  261.     ни в каком базовом классе....
  262.  
  263.     его желательно "видеть" в самом тесте
  264.  
  265.     часто даже не стоит такие вещи в @Before выносить, не смотря на то что код будет не DRY
  266.     но будет еще более явным и очевидным)
  267.  
  268.     Хотя в этом случае наверное с @Before будет таки лучше )
  269.  
  270.     а в классы с именем BaseTest - желательно выносить что-то такое что однозначно общее для всех тестов,
  271.     какие-то настройки может конфигурационные, которые в контексте тест логики смысла не имеют
  272.     и их не грех "скрыть с глаз долой"
  273.  
  274.     Вот и получаем - что от предка тест-класса - избавились)
  275. */
  276. ***********************************************
  277. public class ConciseAPI {
  278.  
  279.     public static Invocation.Builder requestTo(String uri) {
  280.  
  281.     public static Invocation.Builder authorized(Invocation.Builder requestBuilder){
  282. /*
  283.     вот эти методы - универсальные (если "miguel:python" - вынести как параметр метода, а не зашивать это внутри)
  284.     их лучше реализовать в отдельном классе, например RestHelpers
  285.  
  286.     а уже в классе-ресурсе - реализуешь authorizedRequest(String uri)
  287.     в котором вызывай универсальный метод и указывай "miguel:python"
  288.  
  289.     в данном случае - "miguel:python" - это не тестовые данные -
  290.     т к только так и можно авторизироваться
  291.     если бы были варианты - то уж работали бы - как с обычними тестовыми данными
  292.     класс-ресурс тогда ничего про них не должен знать(напрямую - не обращаться)
  293.     а получать такое - через параметры, то ли методов, то ли скоего конструктора - если речь о ресурсе-объекте
  294.  
  295. */
  296. *****************************************************
  297.     public static void createTask(Task newTask){
  298. /*
  299.     Соглашусь, разумно сразу в методе-действии - проверять респонс
  300.     т к вариантов тут нету - эти проверки не зависят от тестового контекста
  301.  
  302.     Правильный статус ответа на каждый из запросов - строго определенный
  303.         И если мы будем проверять статут ответа в тест-методе - то надо об этом думать и вспоминать - какой статус в каких случаях верный
  304.         И можно выкрутиться - проверять статус ответа в нутри степа, а возвращать собственно entity из запроса.
  305.         Сразу пара потенциально полезных моментов)
  306.  
  307.     Если встроить проверку response кода прямо в сам степ
  308.     Тут сразу появляется интересный нюанс
  309.     Дело в том, что мы так спрячем тестовую логику внутрь степа, что не очень хорошо
  310.     С другой стороны - эта проверка = само собой разумеющееся
  311.     Эта проверка достаточно стандартная и всегда будет повторяться после вызова степа (если будет жить вне степа)
  312.     А раз так - то почему бы и не спрятать?
  313.     На самом деле - нет однозначного ответа на этот вопрос...
  314.     Так - со спрятанной проверкой - код станет проще во всех тестах - но втанет до некоторой степени “магическим”
  315.     Тут надо решать самостоятельно - как лучше )
  316.     На твое усмотрение
  317.  
  318.     Еще момент
  319.     В этих API тестах - самих тестов может быть не так уж и много -
  320.     чтоб еще и заморачиваться над DRY кода...нужно и этот аспект учитывать
  321.     Но если пойти этим путем = прятать в степе проверку статуса response, то код будет намного более удобным
  322.     Собственно, ты так и сделал)
  323.     Эти пояснения - для понимания - какие есть варианты
  324.  
  325.     Насчет имени параметра
  326.     Я бы использовала просто task
  327.     ведь ясно - что только новую таску можно создать )
  328.  
  329. */
  330. *************************
  331.     public static List<Task> getListTasks(){
  332. /*
  333.     я бы просто метод назвала - get
  334.  
  335.     с учетом имени ресурса-модуля - будет вполне ок
  336. */
  337. *************************************
  338. public static  void assertTasks(Task ...tasks)
  339. /*
  340.     реализовал оптимально)
  341.  
  342.     единственное - форматирование кода подправь
  343.     и отступы между методами и между блоками кода - тоже единообразнее сделай(это автоматом при реформатировании не поправляется)
  344.  
  345.     как реформатировать = выделить код + в меню Code->reformat code
  346. */
  347. ************************************
  348.  public static void updateTask(int id, Task updatedTask){
  349.  /*
  350.    id - передавать не надо
  351.    у updatedTask (кстати, подусай над именем параметра) - есть uri
  352.    этого достаточно
  353.  
  354.    просто передавай как параметр - таску с верным uri
  355.    и все ок будет
  356.         Task task = new Task("t2 title", "t2 description", true, TasksApi.uri + "/2");
  357.         TasksApi.update(task);
  358.  
  359.     тут - унас нету задачи - получить все таски, найти нужную по uri
  360.     и для нее вызвать update
  361.  
  362.     мы уже знаем - какую таску мы обновляем
  363.     мы уже рассчитвываем на какое-то начальное состояние
  364.  
  365.     потому- тут - реализуй только update конкретной таски
  366.  
  367.     а потом - просто проверть результаты
  368.     кстати - поступай единообразно
  369.  
  370.     если решил проверять результаты респонса внутри степ-методов - то так и дальше делай
  371.  
  372.     а в самом тесте - проверишь тестовую ситуацию = состояние всего списка тасок
  373.     вот это - не скрывай в степ-методе точно
  374.  
  375.  */
  376.  ************************************************
  377.  public static void deleteTask(int id){
  378.  /*
  379.     Тут действительно проще оперировать id
  380.  
  381.     только тоже - не нужно никаких получений списков и их обходов
  382.  
  383.     достаточно - собрать uri - по ид-шнику
  384.     все данные у тебя есть
  385.  
  386.     ну и проверь респонс
  387.  
  388.  */
  389.  ******************************
  390.  public class RestfullTasksTest extends BaseTest {
  391. /*
  392.     Не забывай о правилах CamelCase
  393.     https://google.github.io/styleguide/javaguide.html#s5.3-camel-case
  394.  
  395.     название - RestFulTasksTest - будет ок
  396.  
  397.     посмотри на написание термина RESTful
  398.     https://en.wikipedia.org/wiki/Representational_state_transfer
  399. */
  400. *********************************
  401.      public static final String URI = "http://localhost:5000/todo/api/v1.0/tasks";
  402. /*
  403.     про это выше написано - как разбить и где расположить
  404.  
  405.     uri - нужен всем степ-методам и тест-методам
  406.  
  407.     т е - он тоже инструмент ресурс-модуля
  408.     который еще и настраивается через Configuration.baseUri
  409. */
  410. *******************************************
  411. assertEquals("Unauthorized access", response.readEntity(ErrorContainer.class).getError());
  412. /*
  413.     раз так реализована проверка текста ошибки
  414.     тогда на уровне класса ErrorContainer - тебе не нужы методы equals & hashCode
  415. */
  416. ******************************************
  417.     @Test
  418.     public void  testReadTasks(){
  419.         assertTasks(defaultTask1, defaultTask2);
  420.     }
  421. /*
  422.     технически - так и есть
  423.     делаем гет и проверку
  424.  
  425.     но тут - в testRead - получаем сокрытие тестовой логики
  426.  
  427.     тестовое действие = гет
  428.     проверка - assertEquals(...) - сравни ожидаемые результаты и результатами из респонса
  429.  
  430.     правильно = когда каждое тетсовое действие и каждая проверка - отражались в тест-методе
  431.     Иначе - не ясно из кода тест-метода - что ж мы таки тут делаем
  432.  
  433.     да, мы иногда позволяем себе заворачивать проверки - в метод-действие и наоборот
  434.     но - это исключения из общего правила
  435.     и мы это делаем всегда - зачем-то или почему-то
  436.     бездумно и бесцельно - не скрывай в шагах проверки и в проверках шаги
  437.  
  438.     а вот в остальных тест-методах - использование метода assertTasks - как раз уместно
  439.     мы скрыли технические детали проверки
  440.  
  441.     а тут - используя assertTasks - мы скрыли - тестовую логику
  442.  
  443.     важно эту разницу понимать
  444.  */
  445.  **********************
  446.  public void testCreate(){
  447.  /*
  448.     реализуй 2 варианта теста - создание таски, только по ее title
  449.     и создание таски по полной информации
  450.  
  451.     для остальных операций - достаточно одного теста (1 операция = 1 тест, ну и один е2е)
  452.  */
  453.  ********************************************
  454.      @Test
  455.      public  void testCreateUpdateDelete(){
  456.          Task newTask = new Task("New task item", false, "Title new task", 3);
  457.          Task updateTask = new Task("Upd task", false, "Some title", 3);
  458.  
  459.          //create
  460.          createTask(newTask);
  461.  
  462.          //update
  463.          updateTask(3, updateTask);
  464.  
  465.          //delete
  466.          deleteTask(1);
  467.  
  468.          assertTasks(defaultTask2, updateTask);
  469.      }
  470.      
  471. /*
  472.     советую объявлять и инициализировать Task newTask и Task updateTask - непосредственно перео выполнением операции
  473.     а не в начале метода
  474.    
  475.     цель - чтоб не бегать глазами по коду
  476.     где нужно - там и видим
  477.    
  478.     не забывай - каждая операция в е2е - должна быть проверена)
  479.     и проверка должна касаться всего списка тасок, а не одной таски
  480.     т к возможно - после выполнения операции  над одной таской
  481.     состояние других - исказилось
  482.    
  483.     подумай - что дают комментарии
  484.    
  485.     я бы удалила третью таску
  486.     т к в фиче-тесте - мы уже удаляли таску - созданную по умолчанию
  487.     а тут - удалим таску, созданную вручную
  488.     (это станет важно - т к начальное состояние - будет задаваться спец запросом)
  489. */    
  490.  
  491. ****************************************************
  492. /*
  493.     http://joxi.ru/nAyqEx7HXvxQoA
  494.  
  495.     вот пример хорошей структуры проекта
  496.  
  497.     в src \ main
  498.  
  499.       core - универсальное, что можно переиспользовать в разных проектах
  500.       pages - пейджи тоже можно переиспользовать для других тестов этого же приложения
  501.  
  502.  
  503.     в src \ test
  504.  
  505.       testdata - тестовые данные (если такие есть и они вынесены в отдельный класс)
  506.       testconfigs - предки тест-класса (так можно их изолировать от  собственно тест-классов - чтоб легче было ориентироваться
  507.  
  508.  
  509.     про пекеджи еще немного)
  510.     если GroupID = com.somesite
  511.     а проект todomvctest
  512.     то пакет корневой должен быть com.somesite.todomvctest
  513.  
  514.     логика  - чтобы "не смешивались имена сущностей"
  515.  
  516.     внутри одной компании - может быть несколько проектов)
  517.     и у всех у них один com.somesite  - базовый пекедж
  518.     но для каждого проекта должен быть свой  “базовый пекедж проекта"
  519.     иначе все смешается)
  520.     важно то, что когда этот проект выльется в отдельную библиотеку,
  521.     то не будет конфликтов при его подключении
  522. */
Advertisement
Add Comment
Please, Sign In to add comment