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 AssuredApache HttpClientOkHttpSpring 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.