Огляд Karate – фреймворка для автоматизації тестування API
Усім привіт, мене звати Роман Любунь. Понад 15 років я займаюся автоматизацією, останні три з яких спеціалізуюся на впровадженні автоматизації тестування API на різних проектах. У цій статті розповім про молодий (~1,5 року) фреймворк — Karate, а також чому саме він був обраний для автоматизації інтеграційного тестування на моєму проекті.
Інформація буде корисною всім, кому потрібно:
- легко розпочати API автоматизацію;
- швидко збільшити тестове покриття (API automated tests) та нарощувати його синхронно з розробкою;
- писати BDD тест-кейси/тести для автоматизації;
- перевикористовувати тести для навантажувального тестування;
- автоматизувати тести без досвіду програмування (але у будь-якому випадку вам потрібно вивчити основи REST API, XPath, regular expressions та знати Karate DSL синтаксис);
- перевіряти JSON Schema.
Зараз я працюю на R&D проекті, де розробка ведеться з нуля, а замовник надає пріоритет якості над кількістю фіч (feature). Як результат, з’явилася можливість спробувати кожен з найбільш відомих API automation 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 Integer id; @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 with filled 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 та Cucumber.
Karate DSL
Karate написаний на Java поверх Cucumber-JVM (доступні всі можливості та звітування).
Розробник: Peter Thomas
GitHub-рейтинг: ~1.3k stars
Поточна версія: 0.8.0.1
Ліцензія: MIT
Основні можливості:
- DDT testing;
- ‘natively’ support JSON, XML;
- JsonPath, XPath expressions;
- Schema validation;
- Can call JS functions, Java methods;
- Tags for tests grouping;
- Parallel execution;
- Java API (e.g. call *.feature from Selenium tests);
- Tests can be reused for Gatling performance tool;
- Complex HTTP calls: SOAP/XML requests, HTTPS/SSL (without needing certificates, key-stores or trust-stores), HTTP proxy server support, URL-encoded HTML-form data, multi-part file-upload, browser-like cookie handling, full control over HTTP headers, path and query parameters;
- GraphQL API testing;
- IDE support (with autocomplete);
- Support TestNG/JUnit;
- Built-in environment switcher;
- Tool for step-by-step debugging;
- Integration with CI/CD pipelines;
- Build-in test report (feature > scenario traceability matrix);
- API mock server for test-doubles;
- Mock HTTP servlet for testing any controller servlet such as Spring Boot / MVC or Jersey / JAX-RS — without having to boot an app-server.
Приклад роботи автозаповнення
Повний список можливостей та документація.
Приклади Karate тестів
Простий тест для перевірки CRUD (тестування базових методів: POST, GET, PUT, DELETE + групування тестів):
@smoke # Можна додавати власні теги для групування тестів. Scenario: Delete Vehicle by ID * url baseUrl + 'vehicle' Given path id 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 path id 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 = 'administrator' * 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 path id 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):
- загальний по всіх *.feature-файлах;
- тільки список помилок;
- по тегах;
- індивідуальні звіти для кожного *.feature-файлу.
Звіт по фічах
Звіт по тегах
Звіт тесту з помилкою
Висновки
На основі розроблених тестів можна зробити такі висновки: основна перевага Karate в простоті та наочності тестів, великій кількості можливостей «з коробки» та гнучких перевірках.
Karate добре підходить для:
- швидкого старту API автоматизації; особливо, якщо QA інженери мають мінімальний досвід програмування;
- проектів з тест-кейсами в BDD форматі;
- проектів, на яких ці ж сценарії використовуються для навантажувального тестування.
Менше підходить:
- якщо потрібно тісно інтегрувати UI та API автоматизацію;
- якщо API фреймворк додатково потребує складних функціональних рішень на Java/JavaScript.
Основні недоліки:
- підтримуються лише JS, Java як допоміжні мови;
- наразі сфера використання обмежується лише API;
- умови (if..else) можна реалізувати лише через JavaScript/Java;
- для збереження «читабельності» тестів потрібно дублювати код.
Усім дякую. Сподіваюсь, ця стаття допоможе вам у виборі оптимального API фреймворка. Якщо у вас виникнуть питання, зауваження чи уточнення, буду радий відповісти.