REST Assured (как обсуждается в API Testing Mastery: From REST to Contract Testing) стал де-факто стандартом для тестирования API в экосистеме Java, предоставляя fluent, читаемый DSL (Domain Specific Language) для тестирования REST и HTTP-based сервисов. Это всеобъемлющее руководство исследует REST Assured от основ до продвинутых реализаций корпоративного уровня.
Почему REST Assured для Тестирования API?
REST Assured преодолевает разрыв между тестированием API и разработкой на Java, предлагая нативную интеграцию с существующими Java фреймворками тестирования и инструментами сборки:
- Java-native решение: Бесшовная интеграция с JUnit (как обсуждается в Allure Framework: Creating Beautiful Test Reports), TestNG, Maven и Gradle
- BDD-стиль синтаксис: Структура Given-When-Then для читаемых тестовых сценариев
- Богатая библиотека assertions: Поддержка Hamcrest matchers и JsonPath/XmlPath
- Обработка аутентификации: Встроенная поддержка OAuth, Basic Auth и кастомных схем
- Переиспользование спецификаций: Определить общие request/response спецификации один раз
- Производительность: Прямая реализация HTTP клиента без overhead’а браузера
REST Assured vs Другие Java Фреймворки Тестирования
Функция | REST Assured | Apache HttpClient | OkHttp | Spring RestTemplate |
---|---|---|---|---|
Читаемость | Отличная (BDD) | Низкая | Средняя | Средняя |
Кривая Обучения | Низкая | Высокая | Средняя | Низкая |
Валидация Ответов | Встроенная | Ручная | Ручная | Ручная |
Парсинг JSON/XML | Нативный | Внешние библ. | Внешние библ. | Jackson |
Аутентификация | Комплексная | Ручная | Ручная | Встроенная |
Интеграция Test Framework | Отличная | Ручная | Ручная | Хорошая |
Начало Работы с REST Assured
Maven Зависимость
<dependencies>
<!-- REST Assured -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.5.0</version>
<scope>test</scope>
</dependency>
<!-- JSON Schema Validation -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-schema-validator</artifactId>
<version>5.5.0</version>
<scope>test</scope>
</dependency>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
Конфигурация Gradle
dependencies {
testImplementation 'io.rest-assured:rest-assured:5.5.0'
testImplementation 'io.rest-assured:json-schema-validator:5.5.0'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}
test {
useJUnitPlatform()
}
Базовые Паттерны Запросов
Простой GET Запрос
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
public class BasicAPITests {
@Test
public void testGetUser() {
given()
.baseUri("https://api.example.com")
.header("Accept", "application/json")
.when()
.get("/users/1")
.then()
.statusCode(200)
.body("id", equalTo(1))
.body("email", notNullValue())
.body("name", containsString("John"));
}
}
POST Запрос с Body
@Test
public void testCreateUser() {
String requestBody = """
{
"name": "Jane Doe",
"email": "jane@example.com",
"role": "admin"
}
""";
given()
.contentType(ContentType.JSON)
.body(requestBody)
.when()
.post("/users")
.then()
.statusCode(201)
.body("id", notNullValue())
.body("name", equalTo("Jane Doe"))
.body("email", equalTo("jane@example.com"));
}
Использование POJOs (Plain Old Java Objects)
// Модель User
public class User {
private String name;
private String email;
private String role;
// Конструкторы, getters, setters
public User(String name, String email, String role) {
this.name = name;
this.email = email;
this.role = role;
}
// Getters и setters опущены для краткости
}
@Test
public void testCreateUserWithPOJO() {
User newUser = new User("Alice Johnson", "alice@example.com", "user");
User createdUser =
given()
.contentType(ContentType.JSON)
.body(newUser)
.when()
.post("/users")
.then()
.statusCode(201)
.extract()
.as(User.class);
assertThat(createdUser.getName(), equalTo("Alice Johnson"));
assertThat(createdUser.getEmail(), equalTo("alice@example.com"));
}
Продвинутая Обработка Запросов
Query Parameters и Path Parameters
@Test
public void testQueryParameters() {
given()
.queryParam("page", 2)
.queryParam("limit", 10)
.queryParam("sort", "name")
.when()
.get("/users")
.then()
.statusCode(200)
.body("data.size()", equalTo(10))
.body("pagination.page", equalTo(2));
}
@Test
public void testPathParameters() {
int userId = 123;
String resourceId = "abc-456";
given()
.pathParam("userId", userId)
.pathParam("resourceId", resourceId)
.when()
.get("/users/{userId}/resources/{resourceId}")
.then()
.statusCode(200)
.body("userId", equalTo(userId))
.body("resourceId", equalTo(resourceId));
}
Механизмы Аутентификации
// Базовая Аутентификация
@Test
public void testBasicAuth() {
given()
.auth().basic("username", "password")
.when()
.get("/protected")
.then()
.statusCode(200);
}
// Bearer Token
@Test
public void testBearerToken() {
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
given()
.auth().oauth2(token)
.when()
.get("/api/secure-endpoint")
.then()
.statusCode(200);
}
// Preemptive Аутентификация
@Test
public void testPreemptiveAuth() {
given()
.auth().preemptive().basic("admin", "admin123")
.when()
.get("/admin/dashboard")
.then()
.statusCode(200);
}
// Аутентификация через Кастомный Header
@Test
public void testCustomHeaderAuth() {
given()
.header("X-API-Key", "your-api-key-here")
.header("X-Client-ID", "client-123")
.when()
.get("/api/data")
.then()
.statusCode(200);
}
Request и Response Спецификации
Устраните дублирование с переиспользуемыми спецификациями:
public class APITestBase {
protected static RequestSpecification requestSpec;
protected static ResponseSpecification responseSpec;
@BeforeAll
public static void setup() {
requestSpec = new RequestSpecBuilder()
.setBaseUri("https://api.example.com")
.setBasePath("/api/v1")
.addHeader("Accept", "application/json")
.addHeader("Content-Type", "application/json")
.setAuth(oauth2("access-token"))
.setContentType(ContentType.JSON)
.build();
responseSpec = new ResponseSpecBuilder()
.expectStatusCode(200)
.expectResponseTime(lessThan(2000L))
.expectHeader("Content-Type", containsString("json"))
.build();
RestAssured.requestSpecification = requestSpec;
RestAssured.responseSpecification = responseSpec;
}
}
// Использование спецификаций
@Test
public void testWithSpecifications() {
given()
.spec(requestSpec)
.queryParam("filter", "active")
.when()
.get("/users")
.then()
.spec(responseSpec)
.body("data", notNullValue());
}
Валидация JSON Ответов
JsonPath Запросы
@Test
public void testComplexJSONValidation() {
given()
.get("/api/products")
.then()
.statusCode(200)
// Проверить размер массива
.body("products.size()", greaterThan(0))
// Проверить вложенные значения
.body("products[0].name", notNullValue())
.body("products[0].price", greaterThan(0f))
// Найти конкретные элементы
.body("products.findAll { it.category == 'electronics' }.size()", greaterThan(0))
// Проверить что все элементы соответствуют условию
.body("products.every { it.price > 0 }", equalTo(true))
// Суммировать значения
.body("products.collect { it.price }.sum()", greaterThan(100f));
}
@Test
public void testNestedJSONPaths() {
given()
.get("/api/orders/123")
.then()
.body("order.id", equalTo(123))
.body("order.customer.name", equalTo("John Doe"))
.body("order.items.size()", equalTo(3))
.body("order.items[0].productId", notNullValue())
.body("order.shipping.address.city", equalTo("New York"))
.body("order.payment.status", equalTo("completed"));
}
Извлечение Значений для Повторного Использования
@Test
public void testExtractAndReuse() {
// Извлечь одно значение
String userId =
given()
.body(newUser)
.when()
.post("/users")
.then()
.statusCode(201)
.extract()
.path("id");
// Использовать извлечённое значение в последующем запросе
given()
.pathParam("id", userId)
.when()
.get("/users/{id}")
.then()
.statusCode(200)
.body("id", equalTo(userId));
}
@Test
public void testExtractComplexResponse() {
Response response =
given()
.get("/api/products")
.then()
.statusCode(200)
.extract()
.response();
// Извлечь несколько значений
List<String> productNames = response.jsonPath().getList("products.name");
List<Float> prices = response.jsonPath().getList("products.price", Float.class);
Map<String, Object> firstProduct = response.jsonPath().getMap("products[0]");
// Assertions на извлечённых данных
assertThat(productNames, hasSize(greaterThan(0)));
assertThat(prices, everyItem(greaterThan(0f)));
assertThat(firstProduct, hasKey("id"));
}
Валидация JSON Schema
import static io.restassured.module.jsv.JsonSchemaValidator.*;
@Test
public void testJSONSchemaValidation() {
given()
.get("/api/users/1")
.then()
.statusCode(200)
.body(matchesJsonSchemaInClasspath("schemas/user-schema.json"));
}
// user-schema.json
/*
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["id", "email", "name"],
"properties": {
"id": {
"type": "integer"
},
"email": {
"type": "string",
"format": "email"
},
"name": {
"type": "string",
"minLength": 1
},
"role": {
"type": "string",
"enum": ["admin", "user", "guest"]
}
}
}
*/
Data-Driven Тестирование
Параметризованные Тесты с JUnit 5
@ParameterizedTest
@CsvSource({
"1, John Doe, john@example.com",
"2, Jane Smith, jane@example.com",
"3, Bob Johnson, bob@example.com"
})
public void testMultipleUsers(int id, String name, String email) {
given()
.pathParam("id", id)
.when()
.get("/users/{id}")
.then()
.statusCode(200)
.body("name", equalTo(name))
.body("email", equalTo(email));
}
@ParameterizedTest
@MethodSource("provideUserData")
public void testUserCreation(User user) {
given()
.contentType(ContentType.JSON)
.body(user)
.when()
.post("/users")
.then()
.statusCode(201)
.body("email", equalTo(user.getEmail()));
}
private static Stream<User> provideUserData() {
return Stream.of(
new User("Alice", "alice@test.com", "user"),
new User("Bob", "bob@test.com", "admin"),
new User("Charlie", "charlie@test.com", "moderator")
);
}
TestNG DataProvider
@DataProvider(name = "userData")
public Object[][] userData() {
return new Object[][] {
{"admin", "admin@example.com", 201},
{"user", "user@example.com", 201},
{"", "invalid", 400} // Негативный тест-кейс
};
}
@Test(dataProvider = "userData")
public void testUserCreationWithDataProvider(String name, String email, int expectedStatus) {
User user = new User(name, email, "user");
given()
.contentType(ContentType.JSON)
.body(user)
.when()
.post("/users")
.then()
.statusCode(expectedStatus);
}
Загрузка и Скачивание Файлов
Загрузка Файлов
@Test
public void testFileUpload() {
File imageFile = new File("src/test/resources/test-image.jpg");
given()
.multiPart("file", imageFile, "image/jpeg")
.multiPart("description", "Test image upload")
.when()
.post("/api/upload")
.then()
.statusCode(200)
.body("filename", equalTo("test-image.jpg"))
.body("size", greaterThan(0));
}
@Test
public void testMultipleFileUpload() {
File file1 = new File("src/test/resources/doc1.pdf");
File file2 = new File("src/test/resources/doc2.pdf");
given()
.multiPart("files", file1)
.multiPart("files", file2)
.when()
.post("/api/upload/multiple")
.then()
.statusCode(200)
.body("uploaded.size()", equalTo(2));
}
Скачивание Файлов
@Test
public void testFileDownload() {
byte[] fileContent =
given()
.pathParam("fileId", "abc123")
.when()
.get("/api/download/{fileId}")
.then()
.statusCode(200)
.header("Content-Type", "application/pdf")
.extract()
.asByteArray();
assertThat(fileContent.length, greaterThan(0));
// Сохранить в файл
File downloadedFile = new File("target/downloaded.pdf");
Files.write(downloadedFile.toPath(), fileContent);
}
Тестирование Производительности и Времени Ответа
@Test
public void testResponseTime() {
given()
.get("/api/products")
.then()
.statusCode(200)
.time(lessThan(500L), TimeUnit.MILLISECONDS);
}
@Test
public void testPerformanceMetrics() {
long start = System.currentTimeMillis();
Response response =
given()
.get("/api/heavy-operation")
.then()
.statusCode(200)
.extract()
.response();
long responseTime = response.getTime();
long totalTime = System.currentTimeMillis() - start;
assertThat(responseTime, lessThan(1000L));
assertThat(totalTime, lessThan(1500L));
System.out.println("Время ответа: " + responseTime + "ms");
System.out.println("Общее время: " + totalTime + "ms");
}
Интеграция CI/CD
Конфигурация Maven Surefire
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<includes>
<include>**/*Test.java</include>
<include>**/*Tests.java</include>
</includes>
<systemPropertyVariables>
<api.base.uri>${env.API_BASE_URI}</api.base.uri>
<api.token>${env.API_TOKEN}</api.token>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
Jenkins Pipeline
pipeline {
agent any
environment {
API_BASE_URI = credentials('api-base-uri')
API_TOKEN = credentials('api-token')
}
stages {
stage('Checkout') {
steps {
git 'https://github.com/yourorg/api-tests.git'
}
}
stage('API Tests') {
steps {
sh 'mvn clean test -Dapi.base.uri=${API_BASE_URI} -Dapi.token=${API_TOKEN}'
}
}
stage('Publish Reports') {
steps {
junit '**/target/surefire-reports/*.xml'
publishHTML([
reportDir: 'target/site/serenity',
reportFiles: 'index.html',
reportName: 'API Test Report'
])
}
}
}
}
GitHub Actions
name: API Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Run API Tests
env:
API_BASE_URI: ${{ secrets.API_BASE_URI }}
API_TOKEN: ${{ secrets.API_TOKEN }}
run: mvn clean test
- name: Publish Test Results
uses: dorny/test-reporter@v1
if: always()
with:
name: API Test Results
path: target/surefire-reports/*.xml
reporter: java-junit
Лучшие Практики
Организация Тестов с Page Object Pattern
// UserEndpoint.java
public class UserEndpoint {
private static final String BASE_PATH = "/users";
public Response getAllUsers() {
return given()
.spec(APITestBase.requestSpec)
.when()
.get(BASE_PATH)
.then()
.extract()
.response();
}
public Response getUser(int userId) {
return given()
.spec(APITestBase.requestSpec)
.pathParam("id", userId)
.when()
.get(BASE_PATH + "/{id}")
.then()
.extract()
.response();
}
public Response createUser(User user) {
return given()
.spec(APITestBase.requestSpec)
.body(user)
.when()
.post(BASE_PATH)
.then()
.extract()
.response();
}
}
// Класс теста
public class UserAPITest {
private UserEndpoint userEndpoint = new UserEndpoint();
@Test
public void testGetAllUsers() {
Response response = userEndpoint.getAllUsers();
assertThat(response.statusCode(), equalTo(200));
}
}
Логирование и Отладка
@Test
public void testWithLogging() {
given()
.log().all() // Логировать request
.get("/users")
.then()
.log().all() // Логировать response
.statusCode(200);
}
@Test
public void testConditionalLogging() {
given()
.log().ifValidationFails()
.get("/users")
.then()
.log().ifError()
.statusCode(200);
}
Заключение
REST Assured предоставляет мощное, Java-native решение для тестирования API, которое бесшовно интегрируется в существующие рабочие процессы разработки. Его BDD-стиль (как обсуждается в BDD: From Requirements to Automation) синтаксис, всеобъемлющие возможности валидации и богатая экосистема делают его идеальным для команд, уже инвестировавших в Java stack.
Ключевые преимущества:
- Fluent, читаемый синтаксис тестов
- Глубокая интеграция с экосистемой Java тестирования
- Мощная валидация JSON/XML
- Встроенная поддержка аутентификации
- Отличная интеграция CI/CD
- Активное сообщество и обширная документация
Независимо от того, строите ли вы микросервисы, тестируете REST API или валидируете сложные сценарии интеграции, REST Assured предоставляет инструменты, необходимые для всесторонней, поддерживаемой автоматизации тестирования API.