REST Assured se ha convertido en el estándar de facto para el testing de APIs en el ecosistema Java, ofreciendo un DSL fluido que hace que la validación de solicitudes HTTP complejas sea legible y mantenible. Según el JetBrains State of Developer Ecosystem 2023, Java sigue siendo el lenguaje líder para desarrollo backend con un 45% de adopción, y REST Assured es usado por más del 60% de los proyectos Java con pruebas de API automatizadas. Según el ThoughtWorks Technology Radar, REST Assured ha sido listado consistentemente como herramienta de ‘Trial’ o ‘Adopt’ desde 2015. Para los ingenieros de QA en Java, la sintaxis given-when-then BDD, las aserciones integradas de JSON/XML path, la validación de esquemas y la integración con JUnit y TestNG hacen de REST Assured la elección más productiva.

TL;DR: REST Assured usa la sintaxis BDD given().when().then() para testing de API en Java. Características principales: aserciones JSONPath/XPath, logging de peticiones/respuestas, autenticación (basic, OAuth2, JWT), validación de esquemas e integración con TestNG/JUnit.

¿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

“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

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](/es/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.

FAQ

¿REST Assured funciona solo con APIs REST o puede probar otros protocolos?

REST Assured está diseñado principalmente para APIs HTTP RESTful. Soporta GET, POST, PUT, DELETE, PATCH y otros métodos HTTP con control completo sobre headers, parámetros de consulta y cuerpos de solicitud. Para APIs GraphQL, puedes enviar solicitudes POST con payloads de consulta. Para servicios SOAP, puedes usar payloads XML. Sin embargo, para pruebas gRPC o WebSocket, necesitas herramientas diferentes como grpcurl o clientes WebSocket dedicados.

¿Cómo se compara REST Assured con Postman para pruebas de API?

REST Assured es una biblioteca Java para pruebas programáticas de API que se integra con JUnit y TestNG para pipelines CI/CD. Postman es una herramienta GUI mejor para exploración manual y pruebas ad-hoc rápidas. REST Assured destaca en suites de regresión automatizadas con pruebas basadas en datos, mientras que Postman es mejor para descubrimiento inicial de APIs y documentación. Muchos equipos usan ambos.

¿Cuál es la mejor forma de manejar tokens de autenticación en pruebas REST Assured?

Usa un método @BeforeAll o @BeforeClass para obtener el token una vez y almacenarlo en un RequestSpecification. Esto evita llamadas de autenticación repetidas por cada test. Para OAuth2, usa el método integrado auth().oauth2(token). Para claves API, usa headers. Almacena credenciales en variables de entorno o archivos de configuración, nunca las codifiques directamente en el código de prueba.

¿Puede REST Assured validar esquemas JSON y cuáles son los beneficios?

Sí, REST Assured tiene un módulo dedicado json-schema-validator. Agrega la dependencia y usa matchesJsonSchemaInClasspath() para validar respuestas contra archivos de esquema JSON almacenados en tus recursos de prueba. Esto detecta violaciones de contrato estructural — campos faltantes, tipos incorrectos o valores inesperados — antes de que lleguen a producción, sirviendo como una forma ligera de contract testing.

Ver También

Recursos Oficiales

See Also