REST Assured стал де-факто стандартом для API-тестирования в Java-экосистеме, предлагая свободный DSL, который делает сложную валидацию HTTP-запросов читаемой и поддерживаемой. По данным JetBrains State of Developer Ecosystem 2023, Java остаётся лидирующим языком для backend-разработки с 45% долей рынка, и REST Assured используется более чем в 60% Java-проектов с автоматизированными API-тестами. По данным ThoughtWorks Technology Radar, REST Assured неизменно значится как инструмент для ‘Trial’ или ‘Adopt’ с 2015 года, что отражает его проверенную надёжность в корпоративном API-тестировании. Для Java QA-инженеров синтаксис given-when-then, встроенные утверждения JSON/XML path, валидация схем и бесшовная интеграция с JUnit и TestNG делают REST Assured наиболее продуктивным выбором.

TL;DR: REST Assured использует синтаксис given().when().then() BDD для Java API-тестирования. Основные возможности: утверждения JSONPath/XPath, логирование запросов/ответов, аутентификация (basic, OAuth2, JWT), валидация схем и интеграция с TestNG/JUnit. Начни с: given().baseUri(“https://api.example.com”).when().get("/users").then().statusCode(200).

Почему 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’s given-when-then syntax was revolutionary when it appeared — it made API tests readable by non-technical stakeholders and maintainable by developers, solving both the communication and code quality problems at once.” — Yuri Kan, Senior QA Lead

Начало Работы с 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.

FAQ

REST Assured работает только с REST API или может тестировать другие протоколы?

REST Assured предназначен в первую очередь для RESTful HTTP API. Он поддерживает GET, POST, PUT, DELETE, PATCH и другие HTTP методы с полным контролем над заголовками, параметрами и телом запроса. Для GraphQL можно отправлять POST-запросы с query-нагрузкой. Для SOAP — использовать XML-payload. Но для gRPC или WebSocket тестирования нужны другие инструменты, такие как grpcurl или специализированные WebSocket-клиенты.

Чем REST Assured отличается от Postman для тестирования API?

REST Assured — это Java-библиотека для программного тестирования API, интегрируемая с JUnit и TestNG для CI/CD пайплайнов. Postman — GUI-инструмент для ручного исследования и быстрого ad-hoc тестирования. REST Assured отлично подходит для автоматизированных регрессионных наборов с data-driven тестированием, Postman — для первоначального изучения API и документации. Многие команды используют оба инструмента.

Как лучше всего обрабатывать токены аутентификации в тестах REST Assured?

Используй метод @BeforeAll или @BeforeClass для получения токена один раз и сохранения в RequestSpecification. Это позволяет избежать повторных вызовов аутентификации для каждого теста. Для OAuth2 используй встроенный auth().oauth2(token). Для API-ключей — заголовки. Храни учётные данные в переменных окружения или конфигурационных файлах, никогда не хардкодь их в тестовом коде.

Может ли REST Assured валидировать JSON-схему и в чём преимущества?

Да, у REST Assured есть специальный модуль json-schema-validator. Добавь зависимость и используй matchesJsonSchemaInClasspath() для валидации ответов по JSON-схемам в ресурсах тестов. Это ловит нарушения структурных контрактов — отсутствующие поля, неправильные типы или неожиданные значения — до того, как они попадут в продакшн, выступая как лёгкая форма контрактного тестирования.

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

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

See Also