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ística | REST Assured | Apache HttpClient | OkHttp | Spring RestTemplate |
---|---|---|---|---|
Legibilidad | Excelente (BDD) | Baja | Media | Media |
Curva de Aprendizaje | Baja | Alta | Media | Baja |
Validación de Respuesta | Integrada | Manual | Manual | Manual |
Parseo JSON/XML | Nativo | Libs externas | Libs externas | Jackson |
Autenticación | Completa | Manual | Manual | Integrada |
Integración Framework Tests | Excelente | Manual | Manual | Buena |
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.