Фреймворк для тестування API сервісів на Java — приклад тестового завдання

Усі статті, обговорення, новини про тестування — в одному місці. Підписуйтеся на QA DOU!

Усім привіт! Нещодавно я отримав тестове завдання на позицію QA Automation Team Lead, в якому було необхідно написати тестовий фреймворк для тестування API сервісів на Java. Хоч це і не rocket science, проте мені здалося, що багатьом це може бути цікавим.

По технологіях фреймворк написаний на Java-стеці технологій. Використано maven, Lombok, testNG та Rest Assured.

По архітекторі фреймворк складається з двох шарів (layers), а саме: клієнтський (clients) та тестовий (test) шари. За потреби між ними можна додати бізнес шар (business), якщо API тести ближчі до end-to-end ніж до інтеграційних, або якщо є складна бізнес логіка.

Також за потреби можна додати cucumber шар.

Основна ідея — написати максимально узагальнений базовий клієнт (за допомогою Generics), який містить всі базові методи (GET, POST, PUT and DELETE) і може бути використаний для інтеракції з майже всіма ендпоінтами.

Такий підхід дозволяє зменшити кількість повторювання, а також додати централізоване управління логами Rest Assured (методи configureRequest та configureResponse) через базовий клієнт.

public class BaseAPIClient {
   //some code was cut

   public <T, K> ResponseWrapper<T> getEntity(Class<T> t, RequestWrapper<K> requestWrapper, String url) {
       ResponseWrapper<T> responseWrapper = new ResponseWrapper<>();
       Response response = configureRequest(requestWrapper)
               .when()
               .get(url);

       responseWrapper.setBody(response.as(t));
       responseWrapper.setResponseRaw(response);

       configureResponse(responseWrapper);

       return responseWrapper;
   }

   //some code was cut

   private <T> RequestSpecification configureRequest(RequestWrapper<T> requestWrapper) {
       RequestSpecification requestSpecification = RestAssured.given();

       switch (logRequest()) {
           case "all":
               requestSpecification.log().all();
           case "parameters":
               requestSpecification.log().parameters();
           default:
               requestSpecification.log().method();
       }


       if (requestWrapper.getHeaders() != null) {
           for (String key : requestWrapper.getHeaders().keySet()) {
               requestSpecification.header(key, requestWrapper.getHeaders().get(key));
           }
       }

       if (requestWrapper.getQueryParameters() != null) {
           for (String key : requestWrapper.getQueryParameters().keySet()) {
               requestSpecification.queryParam(key, requestWrapper.getQueryParameters().get(key));
           }
       }

       return requestSpecification;
   }

   //some code was cut
}

Для того, щоб уніфікувати роботу з API клієнтом, створено дві обгортки RequestWrapper та ResponseWrapper.

Ці обгортки узагальненні (generalized) і їх тип визначається у тестах. ResponseWrapper містить і оригінальний респонс у вигляді окремого філду.

@Getter
@Setter
@Builder
public class RequestWrapper<T> {

   private Map<String, String> headers;
   private Map<String, String> queryParameters;

   private T body;
}


@Getter
@Setter
public class ResponseWrapper<T> {
   private T body;
   private Response responseRaw;

   public int getStatusCode() {
       return responseRaw.getStatusCode();
   }
}

BaseAPIClient можна використовувати для інтеракції з різними ендпоінтами, але я раджу створювати окремі клієнти на кожен ендпоінт, щоб логічно розділяти функціонал.

При потребі ці клієнти можна розширювати специфічними методами, такими як пошук та фільтрування. Також можна перезавантажувати базові методи.

public class PostsClient extends BaseAPIClient{
}

При написанні тестів я прихильник принципів ізоляції та керування станом аплікації, тому раджу створювати тестові сутності, які залучені в тесті, в before методі, та видаляти їх в after методі. Це збільшить надійність тестів в рази.

@BeforeMethod
public void init() {
   postsClient = new PostsClient();
   softAsserts = new SoftAssert();

   Post post = Post.builder()
           .userId(1)
           .title("title")
           .body("body")
           .build();

   RequestWrapper<Post> requestWrapper = RequestWrapper.<Post>builder()
           .headers(HEADERS)
           .body(post)
           .build();

   responseWrapperPreconditions = postsClient.postEntity(Post.class, requestWrapper, URL);
}


@AfterMethod
public void cleanup() {
   RequestWrapper<Post> requestWrapper = RequestWrapper.<Post>builder()
           .headers(HEADERS)
           .body(responseWrapperPreconditions.getBody())
           .build();

   postsClient.deleteEntity(Post.class, requestWrapper
           , URL + responseWrapperPreconditions.getBody().getId());
}

Рекомендую розподіляти тести за принципом один REST метод один клас, оскільки прекондішни у такому підході будуть різними і досить великими. Також пропоную групувати перевірки по одному REST методу в одному тестовому методі для оптимізації, хоч це і дискусійне питання. Більш атомарний підхід також має право на життя.

@Test
public void testGetPosts() {
   RequestWrapper<Post> requestWrapper = RequestWrapper.<Post>builder()
           .headers(HEADERS)
           .build();


   ResponseWrapper<Post> responseWrapper = postsClient.getEntity(Post.class, requestWrapper
           , URL + responseWrapperPreconditions.getBody().getId());

   softAsserts.assertEquals(responseWrapper.getStatusCode(), 200
           , "Response status code should be 200");

   softAsserts.assertTrue((isJsonSchemaValid("postsGetSchema.json",
                   convertToStringJSON(responseWrapper.getBody())))
           , "The response should have valid json schema");

   softAsserts.assertEquals(responseWrapper.getBody().getId(), responseWrapperPreconditions.getBody().getId()
           , "Response id should be correct");
   softAsserts.assertEquals(responseWrapper.getBody().getUserId(), responseWrapperPreconditions.getBody().getUserId()
           , "Response userId should be correct");
   softAsserts.assertEquals(responseWrapper.getBody().getTitle(), responseWrapperPreconditions.getBody().getTitle()
           , "Response title should be correct");
   softAsserts.assertEquals(responseWrapper.getBody().getBody(), responseWrapperPreconditions.getBody().getBody()
           , "Response body should be correct");
   softAsserts.assertAll();
}

Оскільки тестовий API тільки симулює створення та видалення сутностей, то тест буде падати і має ілюстраційний характер.

Повний код фреймворку можна знайти за посиланням.

👍ПодобаєтьсяСподобалось21
До обраногоВ обраному10
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

А что проверяет тест, который будет падать? Я так понял, от тестового задания требовалось лишь нанести скелетон проекта?

Тест перевіряє чи GET вертає правильний статус код, правильну сутність та правильну json схему. Падає тому що тестова апішка не вертає ту сутність що ми очікуємо(Бо апішка тестова і насправді не записує сутності в БД)

В тестовому завданні треба було покрити тестами один ендпоінт. Я змінив апішку на іншу для публікації.

При написанні тестів я прихильник принципів ізоляції та керування станом аплікації, тому раджу створювати тестові сутності, які залучені в тесті, в before методі, та видаляти їх в after методі. Це збільшить надійність тестів в рази.

погоджуюсь, але я б це зробив більше ізольовано і динамічно, ніж у вашому варіанті

Post post = Post.builder()
.userId(1)
.title("title")
.body("body")
.build();

я б виніс це в окремий леєр для генерації пейлоаду. це буде зручніше в майбутньому з багатьох причин — масштабування, гнучнкість і реюзабіліті даних і тд.
бо оце трохи хардкод, імхо.
але якщо це чисто для ілюстрації в статті, тоді ок

Також за потреби можна додати cucumber шар.

викинь каку!

Дякую за коментар.

В українській спільноті автоматизаторів прийнято негативно ставитися до кукумберу, але я з такою позицією не згоден.

Негативне ставлення зумовлене тільки тим, що BDD підхід «продається» замовника як та чарівна таблетка. А в 99% випадків ми отримуємо ситуацію, коли бідний автоматизатор і тестує и пише сам ті сценарії і сам їх автоматизацує. В результаті BDD як процесс повністю провалений. А головної болі з підтримки сценаріїв для інженера тільки додається.

Не тільки цим. Я часто бачу негативні повідомлення про кукумберу від спеціалістів, які навіть не працювали з ним. Це як аксіома, просто у спільноті так прийнято. Мені подобається більш широкий погляд на це питання, який полягає в розумінні плюсів і мінусів інструменту.

все ж таки, більшість цих думок базується на реальному досвіді і тільки якась частина — це мейнстрімна думка.
принаймі з мого досвіду я можу точно сказати, що погоджуюсь з негативним ставленням до кукумберу.
отой 1% людей, яким пощастило в житті працювати на проекті з БДД, який працює так як книжка пише, це неймовірне щастя і ніхто ще не зустрічав цих людей.

Я пишу зі свого досвіду — працював на 1 проекті з цим, більше не хочу. На іншому проекті допомагав переїхати з BDD на нормальні тести, які ж усі були щасливі.

При оцінці авто тестів є декілька основних критеріїв, а саме надійність(кількість false positive та true negative результатів), зрозумілість, складність створення та складність підтримки. Цікаво що саме з тим проектом було не так, що прям не хочеться більше мати справу з БДД? І питання чи дійсно проблема була в БДД?

Підписатись на коментарі