Огляд Karate – фреймворку для автоматизації тестування API

Усім привіт, мене звати Роман Любунь. Понад 15 років я займаюся автоматизацією, останні три з яких спеціалізуюся на впровадженні автоматизації тестування API на різних проектах. У цій статті розповім про молодий (~1,5 року) фреймворк — Karate , а також чому саме він був обраний для автоматизації інтеграційного тестування на моєму проекті.

Інформація буде корисною всім, кому потрібно:

Зараз я працюю на R&D проекті, де ведеться розробка з нуля, а замовник надає пріоритет якості над кількістю фіч (feature). Як результат, з'єднання явилася можливість спробувати кожен з найбільш відомих automation API frameworks: REST Assured, Cucumber, Karate — та обрати найбільш оптимальний. Далі я розкажу коротко про проект та чому Karate підійшов найкраще. Також розглянємо можливості Karate-фреймворку, його переваги та недоліки.

Наш проект — це типове на сьогодні рішення: JavaScript + Angular — фронтенд, Java + Spring Boot — бекенд. Команда: три розробникі та один тестувальник.

Підбір фреймворків

Власний фреймворк

Фреймворк представляє собою зв'язку: Rest HTTP client, Lombok, TestNG, AssertJ та Allure. Фреймворк успішно використовувався на багатьох попередніх проектах.

Спрощена архітектура власного фреймворку

Приклад тесту та основних методів:

/** Verify Vehicle's GET method. */
@Test
public void getVehicleTest() {
 Vehicle vehicle = createSimpleVehicle();
 Vehicle receivedVehicle = vehicleService.get(vehicle.getId());

 assertThat(receivedVehicle).as("Created vehicle isn't correct.").isEqualToComparingFieldByField(vehicle);
}

/**
 * Service for Vehicle's creation.
 * @return requested Vehicle object (with ID)
*/
private Vehicle createSimpleVehicle () {
 String UNIQUE_VEHICLE_NAME = "TestVechile" + NumberUtils.getUniqueId();
 Vehicle vehicle = new Vehicle(UNIQUE_VEHICLE_NAME);
 int id = vehicleService.create(vehicle).getId();
vehicle.setId(id);

 return vehicle;
}

/**
 * Vehicle's pojo
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@Data
@EqualsAndHashCode(exclude = "id")
public class Vehicle {
 private id Integer;
@NonNull
 private String name;
 private String description;
}

/**
 * Vehicle's allowed methods
 * @param <T> - response type
*/
public interface VehicleService<T> {
 T create(T sendEntity);

 T get(int id);

 T update(T updatedEntity);

 void delete(int id);

 EntityPagination<T> getAll();
}

/**
* Create a Vehicle only with required parameters.
* @return - ID of created Vehicle.
*/
public int createSimpleVehicle() {
 Vehicle vehicle = buildSimpleVehicle();

 return vehicleService.create(vehicle).getId();
}

/**
* Build Vehicle object filled with required parameters. 
* @return - built Vehicle object
*/
public Vehicle buildSimpleVehicle() {
 return new Vehicle(SIMPLE_PREF + VEHICLE_NAME + NumberUtils.getUniqueId());
}

/**
 * Get entity
 * @param id - entity's ID
 * @return - entity's object
*/
public T get(int id) {
 return HTTP_CLIENT.sendRequest(getEndpoint() + id, HttpMethod.GET, null, getClassName()).getBody();
}

Фреймворк загалом відповідав початковим вимогам:

1. Швидкий старт та покриття функціоналу тестами паралельно з розробкою.

2. Можливість використання тестів (запуск та аналіз execution reports): замовником, розробниками та тестувальником.

Під час розвитку проекту замовник часто змінював основну бізнес-логіку (знайомо, правда? :)), що накладало особливі вимоги на швидкість оновлення тестів. Опис завдань (tasks) був доволі загальними, без деталей та критерій здачі (acceptance criteria). Тому з'єднання явилися додаткові вимоги:

3. Потрібно документувати тест-кейси (раніше я писав лише тести), які використовуватимуться як частина специфікації. Після аналізу та затвердження замовником.

4. Можливість швидко оновлювати тести.

5. Звіт (raceability matrix) повинен відображати покриття функціоналу тестами та наявні дефекти.

Якщо коротко: більше документації та контролю за змінами. При виборі формату тест-кейсів (step — expected result чи given — when — then) замовник надав перевагу BDD.

Rest Assured

Після швидкого аналізу стало зрозуміло, що цей фреймворк економити годину на старті (якщо у вас немає власного) і допомагає організувати тести в BDD стилі.

/** Simple Vehicle creation and verifying Vehicle's GET method. */
@Test
public void getVehicleTest() {
 Vehicle vehicle = createSimpleVehicle();
 int id = vehicle.getId();

given()
 .when().get("/vehicle/" + id)
.then()
.body("name",equalTo(vehicle.getName()))
.statusCode(200);
}

На жаль, навіть добре структуровані тести (у BDD форматі) не читабельні для замовника (особливо перевірки), тому доведеться писати тест-кейси.

Cucumber

BDD тест-кейси:

Feature: Vehicle API implementation

 Scenario: Create vehicle with only required fields filled
 Given Vehicle rest endpoint
 When Vehicle creates with unique name
 Then Return status code 200
 And Vehicle JSON with a new ID number
 And Vehicle JSON with given name

Наявні тести були поділені на кроки:

@When("^Vehicle creates with unique name$")
public int vehicleCreatesWithUniqueName() throws Throwable {
 String UNIQUE_VEHICLE_NAME = "TestVechile" + NumberUtils.getUniqueId();
 Vehicle vehicle = new Vehicle(UNIQUE_VEHICLE_NAME);
 return vehicleService.create(vehicle);
}

Це допомогло виконати вимоги № 3 (Gherkin скрипти замінили тест-кейси) та № 5 (traceability matrix), але ще більше ускладнило № 4 (підтримка актуальності тестів), оскільки до наявного фреймворку додались ще два логічні рівні (кроки та Gherkin скрипти). Як наслідок, суттєво зросли часові витрати на підтримку коду. Після цього я вирішив спробувати найменш відомий із фреймворків.

Karate

Приклад тесту:

Scenario: Create vehicle with only required fields filled
 Given url baseUrl + 'vehicle'
 * def name = 'TestVehicle' + Java.type("utils.NumberUtils").getUniqueId();
 And request { name: '#(name)' }
 When method POST
 Then status 200
 And match response = { id: '#number', name: '#(name)' }

Цей тест виявився доволі читабельним. Після кількох мітингів з'єднання ясувалося, що тести зрозумілі замовнику, тому чудово замінили тест-кейси. Рішення виглядало надто пробачимо, тому я вирішив дослідити цей фреймворк детальніше та спробувати застосувати його в повній мірі на проекті.

Дивіться також більш детальніше-порівняння Karate з REST Assured та Огірок .

Karate DSL

Karate написаний на Java поверх Cucumber-JVM (доступні всі можливості та звітування).

Розробник:
GitHub-рейтинг:~1.3 k stars
Поточна версія:0.8.0.1
Ліцензія:MIT

Основні можливості:

Приклад роботи автозаповнення

Повний список можливостей та документація .

Приклади Karate тестів

Простий тест для перевірки CRUD (тестування базових методів: POST, GET, PUT, DELETE + групування тестів):

@smoke # Можна додавати власні теги для групування тестів.
Scenario: Delete Vehicle by ID
 * url baseUrl + 'vehicle'
 Given id path
 When method DELETE
 Then status 200
 When path id
 And method GET
 Then status 404

DDT тест:

@smoke # Приклад використання декількох тегів
@positive
Scenario Outline: Create an Vehicle with a valid parameters
 # Можна викликати Java метод(і) з тесту.
 * def uniqueNumber = Java.type("utils.NumberUtils").getUniqueId();
 * def name = 'TestVehicle' + uniqueNumber
 * def description = 'Description' + uniqueNumber
 # Запит можна описувати зразу в форматі JSON. Пропадає необхідність використовувати pojo.
 Given request { name: <name>, description: <description> }
 When method POST
 Then status 200
 # Значення name, description братимуться з таблиці нижче.
 And match response == { id: '#number', name: <name>, description: <description> }
 # Тест виконуватиметься стільки разів скільки є рядків у таблиці.
 Examples: 
 | name | description |
 | '#(name)' | '#(description)' |
 | 'TestName' | '#(description)' |
 | 'TestName' | '#(description)' |
 | " | '#(description)' |
 | 'Test_Longest_Possible_Name' | '#(description)' |
 | '#(name)' | null |
 | '#(name)' | " |
 | '#(name)' | 'Test_Longest_Possible_Description' |

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

Недолік:кількість рядків у таблиці фіксована. Це обмеження прийшло з Cucumber фреймворку, на якому базується Karate.

Приклад виклику іншого тесту з pre-conditions:

Feature: Operator API
 # Повторювані передумови можна винести в Background секцію яка виконується на початку шкірного тесту в межах *.feature-файлу.
Background:
 * url baseUrl + 'operator'
 # Створення тестових даних, також, можна винести в окремий файл маючи зручний доступ до його результатів та змінних.
 * def operator = call read('classpath:api/operator/create-smpl-operator.feature')
 * def id = get operator.response.id

 Scenario: Get an Operator by ID
 Given id path
 When method GET
 Then status 200
 # Довгий JSON можна відформатувати для кращої наглядності.
 And match response == 
"""
{
 description: null,
 id: '#number',
 name: '#(operator.operatorJson.name)'
}
"""

Недолік:при перейменуванні чи зміні логіки (порядком виклику) *.feature-файлів IDE не відслідковує залежності. Тому потрібно бути дуже уважним, щоб не «поламати» зв'язки між тестами.

Приклад перевірки JSON-схеми:

Scenario: Validate country's schema
 Given url baseUrl + 'country'
 # Частину вкладеного (nested) JSON можна винести окремо.
 * def oddSchema = {price:'#string', status:'#? _ < 3', ck:'##number', name:'#regex[0-9X]'}
 # Для перевірки складних значень можна викликати сторонні JS функції чі Java методи.
 * def isValidTime = read('time-validator.js')
 When method get
 Then match response ==
"""
{
 id: '#regex[0-9]+', # Підтримка regexp
 count: '#number', # Значення має бути числом
 odd: '#(oddSchema)',
 data: {
 countryId: '#number',
 countryName: '#string', # Значення має бути стрічкою
 leagueName: '##string', # Значення може бути відсутнім або є стрічкою
 status: '#number? _ >= 0',
 sportName: '#string',
 time: '#? isValidTime(_)'
},
 odds: '#[] oddSchema' # Значення має бути масивом
}
"""

Підтримка JsonPath, XPath expressions та RegExp дає широкі можливості написання складних перевірок.

Недолік:доводитися замовнику пояснювати, що означають ті чи інші перевірки.

Приклад авторизації за допомогою Token:

# Тег для позначення *.feature файлів які не є тестами (не потрібно виконувати).
@ignore
Feature: oauth2


 # Функціонал для автентифікації та отримання token
 Scenario: oauth2 authentication
 * url 'https://myserver.com:8443/auth/realms/myproject/protocol/openid-connect/token'
 * form field grant_type = 'password'
 * form field client_id = 'myproject_login_local'
 * form field client_secret = 'f30ca900-ed60-4h7d-8aac-349e432c7b9a'
 * form field username = 'адміністратор'
 * form field password = 'adminPwd'
 * method post
 * status 200
 * def accessToken = response.access_token
 * def authorization = { Authorization: '#("Bearer" + accessToken)' }

Приклад тесту з автентифікацією:

@smoke
Feature: Fleet API


 Scenario: Get Fleet by ID
 # Додавання token (який був згенерований в oauth2.feature) до headers.
 * configure headers = headers.authorization
 * url baseUrl + 'fleet'
 * def fleet = call read('classpath:api/fleet/create-smpl-fleet.feature')
 * def id = get fleet.response.id
 Given id path
 When method GET
 Then status 200

Для перемикання між різними середовищами (environment) використовується karate-config.js (викликається перед кожним тестом):

function() { 
 var env = karate.env; // вибір середовища при запуску з командної стрічки. 


 // середовище за замовчуванням (при виконанні з IDE)
 if (!env) {
 env = 'dev';
}

 var config = {
 env: env,
 // Так можна отримати ім'я сервера наприклад з Java методом.
 // baseUrl – змінна яка використовується як root url у всіх тестах.
 baseUrl: Java.type('utils.PropertyValues').getServerUrl() + ':8086/v1/api/config/'
}
 // Так ми автентифікуємось та отримуєм token перед виконанням тестів.
 config.headers = karate.callSingle('classpath:api/authentication/oauth2.feature', config);


 if (env == 'dev') {
 // в заледжності від подібного оточення можна змінювати глобальні змінні, наприклад: config.foo = 'bar';
 } else if (env == 'e2e') {
 // змінні для іншого оточення
}

 // визначення максимальної тривалості запиту та інші параметри фреймворку.
 karate.configure('connectTimeout', 5000);
 karate.configure('readTimeout', 10000);
 return config;
}

Недолік:тим, хто звик писати тести на Java, потрібно вивчити основи JavaScript.

Паралельне виконання тестів та генерація звіту:

@CucumberOptions(tags = {"~@ignore"})
public class APIParallelExecutorTest {
@Test
 public void executeInParallel() {
 String karateOutputPath = "target/surefire-reports";
 KarateStats stats = CucumberRunner.parallel(getClass(), 100, karateOutputPath);
generateReport(karateOutputPath);
 assertTrue("scenarios failed", stats.getFailCount() == 0);
}


 # Я додатково використовую cucumber-reporter для кращої наглядності.
 private static void generateReport(String karateOutputPath) {
 Collection<File> jsonFiles = FileUtils.listFiles(new File(karateOutputPath), new String[] {"json"}, true);
 List<String> jsonPaths = new ArrayList(jsonFiles.size());
 jsonFiles.forEach(file -> jsonPaths.add(file.getAbsolutePath()));
 Configuration config = new Configuration(new File("target"), "My_project");
 ReportBuilder reportBuilder = new ReportBuilder(jsonPaths, config);
reportBuilder.generateReports();
}
}

Приклад інтеграції UI та API тестів:

// Перевірка створення Fleet через UI.
@Test
public void createFleet() {
 //Перехід на сторінку для створення Fleet.
applicationPage.navigate(BASE_URL);

 //Створення Fleet через UI.
 String fleetName = "TestFleetName" + UNIQUE_ID;
 ConfigurationPage configurationPage = applicationPage.openConfiguration();
configurationPage.addFleet(fleetName);

 //Перевірка чи Fleet створений (через API).
 List<Map<String, Object>> allFleets = fleetApiSteps.getAllFleets();
 int numOfFleetsWithName = filterEntitiesByName(allFleets, fleetName).size();
 Assert.assertTrue(String.format(MSG_MORE_THEN_ONE_FOUND_VIA_API, numOfFleetsWithName, fleetName),
 numOfFleetsWithName == 1);
 //Перевірка чи Fleet з'єднання явився на сторінці (UI).
 Assert.assertTrue(String.format(MSG_NOT_VISIBLE_ON_UI, fleetName),
configurationPage.isFleetDisplayed(fleetName));
}

// Виклик *.feature (API) файлу та опрацювання результату на Java.
public List<Map<String, Object>> getAllFleets() {
 Map<String, Object> result = CucumberRunner.runClasspathFeature("api/fleet/get—all-fleets.feature", null, 'true');
 Map<String, Object> response = (Map<String, Object>) result.get("response");
 return (List<Map<String, Object>>) response.get("content");
}

Відео демо створення та виконання API тестів.

Звітування

Після виконання тестів генерується такий список звітів (після підключення Cucumber reporter):

Звіт по фічах

Звіт по тегах

Звіт тесту з помилкою

Висновки

На основі розроблених тестів можна зробити такі висновки: основна перевага Karate у простоті та наочності тестів, великій кількості можливостей «з коробки» та гнучких перевірках.

Karate добре підходить для:

Менше підходить:

Основні недоліки:

Усім дякую. Сподіваюсь, ця стаття допоможе вам у виборі оптимального API фреймворка. Якщо у вас виникнуть питання, зауваження чи уточнення, буду радий відповісти.

Опубліковано: 13/11/18 @ 11:23
Розділ Різне

Рекомендуємо:

Від збору паперу до сонячної електростанції на терасі. Екоініціативи українських ІТ-компаній
iOS дайджест #29: лонгриды про Jailbreak, Marzipan, Build System, хакі з Твіттера
Різні способи дебага запитів з Android-пристроїв і емуляторів
Як подолати страх публічних виступів: поради бізнес-тренера
Перегляд бізнес-моделі через 3 дні після старту реклами в Фейсбуці