REST Assured (как обсуждается в API Testing Mastery: From REST to Contract Testing) стал де-факто стандартом для тестирования API в экосистеме Java, предоставляя fluent, читаемый DSL (Domain Specific Language) для тестирования REST и HTTP-based сервисов. Это всеобъемлющее руководство исследует REST Assured от основ до продвинутых реализаций корпоративного уровня.

Если вы начинаете с тестирования API, наше руководство Postman: From Manual to Automation предоставляет отличное введение перед переходом к решениям на основе кода. Для сложных архитектур ознакомьтесь с API Testing Architecture in Microservices, а для GraphQL API изучите наш GraphQL Testing Guide.

Почему 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.

Смотрите также

Официальные ресурсы

See Also