REST Assured (como se discute en API Testing Mastery: From REST to Contract Testing) se ha convertido en el estándar de facto para pruebas de API en el ecosistema Java, proporcionando un DSL (Domain Specific Language) fluido y legible para probar servicios REST y basados en HTTP. Esta guía completa explora REST Assured desde fundamentos hasta implementaciones avanzadas a nivel empresarial.

¿Por Qué REST Assured para Pruebas de API?

REST Assured cierra la brecha entre las pruebas de API y el desarrollo Java, ofreciendo integración nativa con frameworks de prueba Java existentes y herramientas de construcción:

  • Solución nativa de Java: Integración perfecta con JUnit, TestNG, Maven y Gradle
  • Sintaxis estilo BDD: Estructura Given-When-Then para escenarios de prueba legibles
  • Biblioteca de aserciones rica: Soporte para Hamcrest matchers y JsonPath/XmlPath
  • Manejo de autenticación: Soporte integrado para OAuth, Basic Auth y esquemas personalizados
  • Reutilización de especificaciones: Definir especificaciones comunes de request/response una vez
  • Rendimiento: Implementación directa de cliente HTTP sin sobrecarga de navegador

REST Assured vs Otros Frameworks de Prueba Java

CaracterísticaREST AssuredApache HttpClientOkHttpSpring RestTemplate
LegibilidadExcelente (BDD)BajaMediaMedia
Curva de AprendizajeBajaAltaMediaBaja
Validación de RespuestaIntegradaManualManualManual
Parseo JSON/XMLNativoLibs externasLibs externasJackson
AutenticaciónCompletaManualManualIntegrada
Integración Framework TestsExcelenteManualManualBuena

Comenzando con REST Assured

Dependencia 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 (como se discute en [Allure Framework: Creating Beautiful Test Reports](/blog/allure-framework-reporting)) 5 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Configuración 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()
}

Patrones Básicos de Solicitudes

Solicitud GET Simple

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"));
    }
}

Solicitud POST con 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"));
}

Usando POJOs (Plain Old Java Objects)

// Modelo User
public class User {
    private String name;
    private String email;
    private String role;

    // Constructores, getters, setters
    public User(String name, String email, String role) {
        this.name = name;
        this.email = email;
        this.role = role;
    }

    // Getters y setters omitidos por brevedad
}

@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"));
}

Manejo Avanzado de Solicitudes

Query Parameters y 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));
}

Mecanismos de Autenticación

// Autenticación Básica
@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);
}

// Autenticación Preemptive
@Test
public void testPreemptiveAuth() {
    given()
        .auth().preemptive().basic("admin", "admin123")
    .when()
        .get("/admin/dashboard")
    .then()
        .statusCode(200);
}

// Autenticación con Header Personalizado
@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 y Response Specifications

Elimina duplicación con especificaciones reutilizables:

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;
    }
}

// Usando especificaciones
@Test
public void testWithSpecifications() {
    given()
        .spec(requestSpec)
        .queryParam("filter", "active")
    .when()
        .get("/users")
    .then()
        .spec(responseSpec)
        .body("data", notNullValue());
}

Validación de Respuestas JSON

Consultas JsonPath

@Test
public void testComplexJSONValidation() {
    given()
        .get("/api/products")
    .then()
        .statusCode(200)
        // Verificar tamaño de array
        .body("products.size()", greaterThan(0))
        // Verificar valores anidados
        .body("products[0].name", notNullValue())
        .body("products[0].price", greaterThan(0f))
        // Encontrar items específicos
        .body("products.findAll { it.category == 'electronics' }.size()", greaterThan(0))
        // Verificar que todos los items cumplen condición
        .body("products.every { it.price > 0 }", equalTo(true))
        // Sumar valores
        .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"));
}

Extrayendo Valores para Reutilizar

@Test
public void testExtractAndReuse() {
    // Extraer valor único
    String userId =
        given()
            .body(newUser)
        .when()
            .post("/users")
        .then()
            .statusCode(201)
            .extract()
            .path("id");

    // Usar valor extraído en solicitud subsecuente
    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();

    // Extraer múltiples valores
    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]");

    // Aserciones sobre datos extraídos
    assertThat(productNames, hasSize(greaterThan(0)));
    assertThat(prices, everyItem(greaterThan(0f)));
    assertThat(firstProduct, hasKey("id"));
}

Validación de 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"]
        }
    }
}
*/

Pruebas Basadas en Datos

Tests Parametrizados con 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} // Caso de prueba negativo
    };
}

@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);
}

Carga y Descarga de Archivos

Carga de Archivos

@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));
}

Descarga de Archivos

@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));

    // Guardar a archivo
    File downloadedFile = new File("target/downloaded.pdf");
    Files.write(downloadedFile.toPath(), fileContent);
}

Pruebas de Rendimiento y Tiempo de Respuesta

@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("Tiempo de respuesta: " + responseTime + "ms");
    System.out.println("Tiempo total: " + totalTime + "ms");
}

Integración CI/CD

Configuración 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

Mejores Prácticas

Organizar Tests con 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();
    }
}

// Clase de test
public class UserAPITest {
    private UserEndpoint userEndpoint = new UserEndpoint();

    @Test
    public void testGetAllUsers() {
        Response response = userEndpoint.getAllUsers();
        assertThat(response.statusCode(), equalTo(200));
    }
}

Logging y Debugging

@Test
public void testWithLogging() {
    given()
        .log().all() // Loguear request
        .get("/users")
    .then()
        .log().all() // Loguear response
        .statusCode(200);
}

@Test
public void testConditionalLogging() {
    given()
        .log().ifValidationFails()
        .get("/users")
    .then()
        .log().ifError()
        .statusCode(200);
}

Conclusión

REST Assured proporciona una solución potente, nativa de Java para pruebas de API que se integra perfectamente en flujos de trabajo de desarrollo existentes. Su sintaxis estilo BDD (como se discute en BDD: From Requirements to Automation), capacidades de validación completas y rico ecosistema lo hacen ideal para equipos ya invertidos en el stack de Java.

Ventajas clave:

  • Sintaxis de test fluida y legible
  • Integración profunda con ecosistema de testing Java
  • Validación potente de JSON/XML
  • Soporte integrado de autenticación
  • Excelente integración CI/CD
  • Comunidad activa y documentación extensiva

Ya sea construyendo microservicios, probando REST APIs, o validando escenarios de integración complejos, REST Assured entrega las herramientas necesarias para automatización de pruebas de API completa y mantenible.