# Día 21: Tests de API y de Carga: MockMvc, REST Assured, BDD y JMeter

> Probar la API como la usa un cliente, escribir escenarios de negocio con Cucumber y medir el rendimiento antes de producción

- Plan: [JavaMastery](https://aprende-java.webs-economicas.es/), día 21 de 30
- Fase 5: Testing de Integración, APIs y Carga
- Dedicación: 4h
- Profundidad: para dominar
- Revisado: 2026-10-29
- Versión interactiva: https://aprende-java.webs-economicas.es/dias/tests-api-carga-mockmvc-rest-assured-jmeter/

## Al terminar este día sabrás

- Probar el contrato HTTP de un endpoint con `@WebMvcTest` y `MockMvc`.
- Escribir un test de recorrido completo con REST Assured y Testcontainers.
- Expresar una regla de negocio como escenario de Cucumber en español.
- Diseñar y leer una prueba de carga con JMeter fijándote en percentiles y errores.


## Conceptos

### 1. MockMvc y @WebMvcTest a Fondo

`@WebMvcTest` arranca solo la capa web y `MockMvc` simula peticiones HTTP sin servidor real: comprueba rutas, validación, códigos de estado, cabeceras y el JSON de respuesta con `jsonPath`. Los servicios se sustituyen con `@MockitoBean`.

**Cómo funciona por dentro.**

`@WebMvcTest(TareaController.class)` crea un contexto de Spring reducido: el controlador indicado, los `@ControllerAdvice`, los convertidores de Jackson, la validación, los filtros y la seguridad web si está configurada. No crea servicios ni repositorios, así que los que necesite el controlador se aportan con `@MockitoBean`. Arranca en uno o dos segundos y cada test tarda milisegundos.

`MockMvc` ejecuta la petición por toda la pila de Spring MVC sin abrir un puerto: el `DispatcherServlet` resuelve la ruta, deserializa el cuerpo, valida, llama al controlador y serializa la respuesta. Con `jsonPath` compruebas campos concretos del JSON, y con `header()`, las cabeceras. Desde Spring Framework 6.2 existe también `MockMvcTester`, con aserciones al estilo AssertJ.

Qué probar aquí: códigos de estado, validación de la entrada, formato de los errores (`ProblemDetail`), cabeceras como `Location`, la serialización de fechas y enums, y las reglas de seguridad de cada endpoint. Qué no: la lógica de negocio (va en tests unitarios del servicio) ni el acceso a datos.

*Contrato de POST /tareas*

```java
@WebMvcTest(TareaController.class)
class TareaControllerTest {

    @Autowired MockMvc mvc;
    @MockitoBean TareaService servicio;

    @Test
    void crear_devuelve_201_con_location() throws Exception {
        given(servicio.crear(any())).willReturn(new TareaDto(42L, "Revisar PR", Estado.PENDIENTE));

        mvc.perform(post("/tareas")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"titulo": "Revisar PR"}
                    """))
            .andExpect(status().isCreated())
            .andExpect(header().string("Location", endsWith("/tareas/42")))
            .andExpect(jsonPath("$.estado").value("PENDIENTE"));
    }

    @Test
    void crear_sin_titulo_devuelve_400_con_problem_detail() throws Exception {
        mvc.perform(post("/tareas")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{}"))
            .andExpect(status().isBadRequest())
            .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON))
            .andExpect(jsonPath("$.status").value(400));
        verifyNoInteractions(servicio);
    }
}
```

- **En una línea:** `mvc.perform(post("/tareas").contentType(APPLICATION_JSON).content("{}")).andExpect(status().isBadRequest()).andExpect(jsonPath("$.title").exists());`
- **Por qué importa:** Prueba en milisegundos el contrato HTTP de cada endpoint: que un DTO inválido devuelve 400 con `ProblemDetail` y que crear devuelve 201 con `Location`.
- **Error típico:** Comprobar solo el código de estado: el endpoint puede devolver 200 con un JSON incorrecto. Comprueba también el cuerpo.
- **Java 17 frente a 21:** Igual en Java 17 y 21. Desde Spring Boot 3.4, `@MockitoBean` sustituye a `@MockBean`, que queda obsoleto.
- **Se conecta con:** @MockitoBean, ProblemDetail, Bean Validation

### 2. REST Assured contra la Aplicación Arrancada

Con `@SpringBootTest(webEnvironment = RANDOM_PORT)` la aplicación arranca de verdad en un puerto libre, y REST Assured le hace peticiones HTTP reales con una sintaxis given/when/then. Junto a Testcontainers, prueba el recorrido completo: HTTP, servicio, base de datos y vuelta.

**Cómo funciona por dentro.**

Con `@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)`, Spring arranca la aplicación completa con un servidor real en un puerto libre, que se inyecta con `@LocalServerPort`. REST Assured hace peticiones HTTP de verdad con una sintaxis given/when/then y valida estado, cabeceras y cuerpo con matchers de Hamcrest, o extrayendo la respuesta a un objeto.

Combinado con Testcontainers y `@ServiceConnection`, el test recorre todo el camino que recorrerá un cliente: filtros de seguridad, serialización real, servicio, transacciones y base de datos PostgreSQL. Detecta errores que ningún test aislado ve: una propiedad de configuración mal escrita, un filtro que bloquea la ruta, una columna que no acepta `null`.

Son tests caros (segundos cada uno), así que se reservan para los flujos críticos de punta a punta. Reutiliza el mismo contexto de Spring entre clases (con la misma configuración, Spring lo guarda en caché) y el mismo contenedor, y limpia los datos entre tests en lugar de reiniciar nada.

*Flujo completo con REST Assured y PostgreSQL real*

```java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class TareasFlujoIT {

    @Container @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");

    @LocalServerPort int port;

    @BeforeEach
    void configurar() {
        RestAssured.port = port;
    }

    @Test
    void crear_consultar_y_completar_una_tarea() {
        String location =
            given().contentType(ContentType.JSON).body("""
                    {"titulo": "Preparar release"}
                    """)
            .when().post("/tareas")
            .then().statusCode(201)
            .extract().header("Location");

        given().when().get(location)
            .then().statusCode(200).body("estado", equalTo("PENDIENTE"));

        given().when().post(location + "/completar")
            .then().statusCode(200).body("estado", equalTo("HECHA"));
    }
}
```

- **En una línea:** `given().port(port).contentType(JSON).body(nueva).when().post("/tareas").then().statusCode(201).header("Location", containsString("/tareas/"));`
- **Por qué importa:** Detecta lo que MockMvc no ve: filtros de seguridad reales, serialización completa, configuración y transacciones contra la base de datos.
- **Error típico:** Escribir así todos los tests de la API: son mucho más lentos. Resérvalos para los flujos críticos y deja la validación a MockMvc.
- **Java 17 frente a 21:** Igual en Java 17 y 21.
- **Se conecta con:** Testcontainers, Spring Security, Tests end-to-end

### 3. BDD con Cucumber-JVM

Cucumber escribe los escenarios en lenguaje natural (Gherkin: `Dado`, `Cuando`, `Entonces`) en ficheros `.feature`, y cada frase se conecta con un método Java (step definition). Admite español con `# language: es`.

**Cómo funciona por dentro.**

Gherkin es el lenguaje de los escenarios: un fichero `.feature` contiene una `Característica` con varios `Escenario`, y cada uno se escribe con pasos `Dado` (contexto), `Cuando` (acción) y `Entonces` (resultado), más `Y` y `Pero` para encadenar. Con `# language: es` en la primera línea, las palabras clave van en español, y un `Esquema del escenario` con una tabla de `Ejemplos` ejecuta el mismo escenario con varios datos.

Cada paso se conecta con un método Java anotado (`@Dado`, `@Cuando`, `@Entonces`) mediante Cucumber Expressions, donde `{string}` o `{int}` capturan parámetros. El estado entre pasos se guarda en los campos de la clase de pasos, que Cucumber crea de nuevo para cada escenario; con `cucumber-spring`, esa clase puede recibir beans de Spring.

Cucumber aporta valor cuando las personas de negocio leen o escriben los escenarios y estos sirven como criterios de aceptación vivos. Si solo los lee el equipo técnico, es una capa de indirección que JUnit con nombres claros y estructura given/when/then resuelve con menos coste.

*Escenario en español y sus pasos en Java*

```java
// src/test/resources/features/completar.feature
// # language: es
// Característica: Completar tareas
//   Escenario: No se puede completar una tarea bloqueada
//     Dado una tarea "Desplegar" bloqueada por "falta aprobación"
//     Cuando intento completarla
//     Entonces veo el error "tarea bloqueada: falta aprobación"
//     Y la tarea sigue pendiente

public class CompletarTareasSteps {
    private final TareaService servicio;            // inyectado con cucumber-spring
    private Tarea tarea;
    private Throwable error;

    public CompletarTareasSteps(TareaService servicio) { this.servicio = servicio; }

    @Dado("una tarea {string} bloqueada por {string}")
    public void unaTareaBloqueada(String titulo, String motivo) {
        tarea = servicio.crear(titulo);
        servicio.bloquear(tarea.id(), motivo);
    }

    @Cuando("intento completarla")
    public void intentoCompletarla() {
        error = catchThrowable(() -> servicio.completar(tarea.id()));
    }

    @Entonces("veo el error {string}")
    public void veoElError(String mensaje) {
        assertThat(error).hasMessage(mensaje);
    }

    @Y("la tarea sigue pendiente")
    public void sigueAbierta() {
        assertThat(servicio.buscar(tarea.id()).estado()).isEqualTo(Estado.PENDIENTE);
    }
}
```

- **En una línea:** `Escenario: No se puede completar una tarea bloqueada` · `Dado una tarea bloqueada` · `Cuando intento completarla` · `Entonces veo el error "tarea bloqueada"`.
- **Por qué importa:** Negocio, QA y desarrollo comparten la misma especificación ejecutable. Es útil cuando las reglas son complejas y las validan personas que no leen Java.
- **Error típico:** Escribir escenarios con detalles técnicos («Cuando hago POST a /tareas con este JSON»): dejan de ser legibles para negocio. Describe el comportamiento, no la implementación.
- **Java 17 frente a 21:** Igual en Java 17 y 21.
- **Se conecta con:** Given/When/Then, Criterios de aceptación, REST Assured

### 4. Pruebas de Carga con JMeter

JMeter simula muchos usuarios concurrentes contra la API y mide tiempos de respuesta, percentiles y errores. El plan se diseña en su interfaz y se ejecuta sin ella (`jmeter -n -t plan.jmx -l resultados.jtl -e -o informe/`). Gatling es una alternativa que escribe los escenarios como código.

**Cómo funciona por dentro.**

Una prueba de carga responde a preguntas concretas: cuántas peticiones por segundo aguanta el servicio, qué latencia tienen el 95 % y el 99 % de las peticiones con esa carga, y qué recurso se agota primero. En JMeter, un plan (`.jmx`) tiene grupos de hilos (usuarios virtuales, rampa de subida y duración), samplers HTTP con las peticiones, extractores para encadenar datos (el id de la tarea creada) y aserciones sobre las respuestas.

El plan se diseña en la interfaz gráfica, pero se ejecuta siempre en modo no interactivo (`-n`), que consume mucho menos y genera un informe HTML con percentiles, rendimiento y errores a lo largo del tiempo. Gatling, k6 y Locust son alternativas que escriben los escenarios como código y encajan mejor en un repositorio y en el CI.

Para que los resultados signifiquen algo, la prueba se lanza contra un entorno con datos y recursos parecidos a producción, desde una máquina distinta a la del servicio, con una rampa de subida y mirando a la vez las métricas del servicio (CPU, memoria, pool de conexiones, GC). El objetivo no es un número aislado, sino encontrar el punto en que la latencia se dispara y entender por qué.

*Ejecutar el plan y vigilar el servicio*

```bash
# Ejecución sin interfaz gráfica: resultados en CSV y un informe HTML
jmeter -n -t carga-tareas.jmx \
  -Jusuarios=200 -Jrampa=60 -Jduracion=300 \
  -l resultados.jtl \
  -e -o informe/

# En el plan, las propiedades se leen con ${__P(usuarios,50)}
# Criterios de aceptación de la prueba:
#   - percentil 95 < 300 ms y percentil 99 < 800 ms
#   - 0 % de errores
#   - ninguna petición esperando conexión de forma sostenida

# Mientras corre, observa el servicio:
curl -s localhost:8080/actuator/metrics/hikaricp.connections.pending
curl -s localhost:8080/actuator/metrics/http.server.requests
```

- **En una línea:** `Plan con 200 usuarios que crean y consultan tareas durante 5 minutos; objetivo: percentil 95 por debajo de 300 ms y 0 % de errores.`
- **Por qué importa:** Los tests funcionales no dicen cuántas peticiones aguanta el servicio. La prueba de carga encuentra el cuello de botella (pool de conexiones, consultas, CPU) antes que los usuarios.
- **Error típico:** Mirar solo la media del tiempo de respuesta: oculta los casos lentos. Mira los percentiles 95 y 99 y la tasa de errores.
- **Java 17 frente a 21:** Igual en Java 17 y 21. Con virtual threads, repite la prueba: el límite suele pasar del pool de hilos a la base de datos.
- **Se conecta con:** Percentiles 95 y 99, Pool de conexiones, Virtual threads

### 5. Qué Automatizar en el Pipeline de CI

En cada push, los tests unitarios y de MockMvc, que son rápidos. En cada pull request, los de integración con Testcontainers y REST Assured. Antes de una release o de forma programada, las pruebas de carga contra un entorno parecido a producción.

**Cómo funciona por dentro.**

Cada tipo de test tiene un coste distinto y debe ejecutarse donde más aporta. En cada push o en el ordenador del desarrollador: unitarios y tests de slice (`@WebMvcTest`, y `@DataJpaTest` con Testcontainers si son rápidos), que deben terminar en pocos minutos. En cada pull request: además, los de integración completos con REST Assured. Antes de una release o cada noche: pruebas de carga y de extremo a extremo contra un entorno de preproducción.

Con Maven, la separación es natural: Surefire ejecuta en la fase `test` las clases `*Test`, y Failsafe ejecuta en `integration-test` y `verify` las clases `*IT`, asegurando que el entorno se limpia aunque fallen. Así `./mvnw test` es rápido y `./mvnw verify` lo ejecuta todo.

Un pipeline útil es rápido, fiable y explica sus fallos: los tests intermitentes se corrigen o se aíslan en el momento, no se reintentan hasta que pasan; los informes (JUnit, JaCoCo, el HTML de JMeter) se publican como artefactos; y un fallo de rendimiento por encima del umbral acordado bloquea la release igual que un test en rojo.

*Workflow de GitHub Actions por etapas*

```yaml
name: ci
on:
  push:
  pull_request:

jobs:
  rapidos:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: 21, cache: maven }
      - run: ./mvnw -B test                      # unitarios y @WebMvcTest

  integracion:
    if: github.event_name == 'pull_request'
    needs: rapidos
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: 21, cache: maven }
      - run: ./mvnw -B verify                    # *IT con Testcontainers y REST Assured
      - uses: actions/upload-artifact@v4
        if: always()
        with: { name: informes, path: target/failsafe-reports }
```

- **En una línea:** `./mvnw test` en cada push y `./mvnw verify` (que ejecuta los `*IT`) en cada pull request.
- **Por qué importa:** Un pipeline que tarda 40 minutos acaba saltándose. Repartir los tests según su coste mantiene la respuesta rápida sin renunciar a la cobertura.
- **Error típico:** Mezclar unitarios e integración en la misma fase. Con Maven, los de integración van con Failsafe (`*IT.java`, fase `verify`) y los unitarios con Surefire (fase `test`).
- **Java 17 frente a 21:** Igual en Java 17 y 21.
- **Se conecta con:** Pirámide de testing, Maven Surefire y Failsafe, GitHub Actions


## Sesión de 90 minutos

### Bloque 1: Contrato HTTP con MockMvc (0 - 25 min)

- **Qué estudiar:** `@WebMvcTest`, `MockMvc`, `jsonPath` y `@MockitoBean`.
- **Pregunta a responder:** ¿Qué debe devolver tu API ante un DTO inválido?
- **Entregable:** Tests de MockMvc para crear una tarea: 201 con `Location` y 400 con `ProblemDetail`.

### Bloque 2: Recorrido Completo con REST Assured (25 - 50 min)

- **Qué estudiar:** `@SpringBootTest(webEnvironment = RANDOM_PORT)` con Testcontainers.
- **Pregunta a responder:** ¿Qué detecta este test que MockMvc no puede ver?
- **Entregable:** Test que crea, consulta y completa una tarea a través de HTTP real.

### Bloque 3: BDD y Carga (50 - 80 min)

- **Qué estudiar:** Un `.feature` en español con Cucumber y un plan de JMeter en modo no interactivo.
- **Pregunta a responder:** ¿Qué percentil y qué tasa de errores vas a exigir?
- **Entregable:** Un escenario de Cucumber de negocio y un informe HTML de JMeter con 100 usuarios durante 2 minutos.

### Cierre de Consolidación (80 - 90 min)

- **Qué estudiar:** Repaso de la rúbrica del tutor.
- **Pregunta a responder:** Explicar qué tipo de test usarías para validación, reglas de negocio, flujo completo y rendimiento.
- **Entregable:** Propuesta de pipeline: qué se ejecuta en cada push, en cada pull request y antes de cada release.


## Explícalo desde cero

### MockMvc frente a REST Assured

- **Qué entender:** MockMvc llama a tus controladores sin abrir un puerto: es rápido y prueba la capa web. REST Assured habla HTTP de verdad con la aplicación entera arrancada: es lento y prueba todo el recorrido. Se usan los dos, cada uno para lo suyo.
- **Qué suele explicarse mal:** Decir que MockMvc «no es un test real». Es real para lo que prueba: el contrato HTTP de los controladores.
- **Ejemplo sencillo:** Probar el volante de un coche en el taller (MockMvc) frente a dar una vuelta por la ciudad (REST Assured).
- **Señal de alerta:** Si todos tus tests de API arrancan la aplicación completa y la suite tarda más de diez minutos.


## Preguntas

### Nivel 1 (Básico): ¿Qué carga `@WebMvcTest` y qué deja fuera?

Carga los controladores, los `@ControllerAdvice`, los filtros, la configuración de Jackson y la validación. Deja fuera servicios, repositorios y base de datos, que se sustituyen con `@MockitoBean`. Por eso es rápido.

**Respuesta razonada:** Carga la parte web de Spring MVC: el controlador indicado (o todos si no indicas ninguno), los `@ControllerAdvice`, los `@JsonComponent`, los convertidores, los filtros, los `WebMvcConfigurer` y la configuración de Spring Security para la web. Deja fuera los `@Service`, `@Repository` y `@Component` normales, el `DataSource` y JPA.

Por eso cualquier dependencia del controlador debe aportarse con `@MockitoBean` o con `@Import` de una configuración concreta; si no, el contexto no arranca por falta del bean. Esa restricción es justo lo que lo hace rápido y centrado en el contrato HTTP.

### Nivel 2 (Intermedio): ¿Por qué la media del tiempo de respuesta es un mal indicador en una prueba de carga?

Porque unas pocas peticiones muy lentas quedan escondidas entre muchas rápidas. El percentil 95 o 99 dice qué tiempo sufre el 5 % o el 1 % de usuarios con peor experiencia, que es donde aparecen los problemas de colas, locks o pools agotados.

**Respuesta razonada:** Porque la media mezcla todas las peticiones y se deja dominar por la mayoría rápida. Si 95 de cada 100 peticiones tardan 50 ms y 5 tardan 4 segundos, la media ronda los 250 ms, un número que no describe a nadie: ni a los usuarios contentos ni a los que esperan 4 segundos.

Los percentiles dicen qué experimenta cada franja: el p95 es el tiempo por debajo del cual quedan el 95 % de las peticiones. Con varias llamadas por pantalla, casi todos los usuarios sufren alguna vez el p99, por eso los objetivos de servicio se expresan en percentiles y se vigilan junto con la tasa de errores.

### Nivel 3 (Entrevista Senior): En la prueba de carga, a partir de 150 usuarios el tiempo de respuesta se dispara, pero la CPU está al 30 %. ¿Dónde buscarías?

En recursos que se agotan sin gastar CPU: el pool de conexiones de la base de datos (HikariCP trae 10 por defecto), el pool de hilos del servidor, locks o llamadas externas lentas sin timeout. Las métricas de Actuator (`hikaricp.connections.pending`, hilos ocupados) y un volcado de hilos lo confirman.

**Respuesta razonada:** Si la latencia se dispara con la CPU baja, los hilos están esperando, no trabajando. El sospechoso habitual es el pool de conexiones a la base de datos: HikariCP trae 10 conexiones por defecto, y con 150 usuarios concurrentes las peticiones hacen cola para conseguir una. Otros candidatos: el pool de hilos del servidor (200 en Tomcat por defecto), locks con mucha contención, llamadas a servicios externos lentos sin timeout o el propio límite de conexiones de la base de datos.

Para confirmarlo: métricas de Actuator como `hikaricp.connections.pending` y `hikaricp.connections.usage`, el número de hilos ocupados de Tomcat y un volcado de hilos (`jcmd <pid> Thread.print`) durante la prueba para ver dónde esperan. Ampliar el pool sin más solo traslada el cuello de botella a la base de datos; a veces la solución es reducir el tiempo que cada petición retiene la conexión.


## Reto del tutor

**Reto:** Un equipo tiene 300 tests de API con REST Assured contra la aplicación completa y una base de datos real. La suite tarda 25 minutos y los desarrolladores se la saltan. Proponen borrar la mitad. Evalúa la situación.

- **Lo que está bien:** Detectan que una suite lenta pierde su valor.
- **Respuesta superficial:** Proponer «paralelizar los tests» como única solución.
- **Lo que falta de fondo:** No ver que la mayoría de esos tests comprueban validaciones y códigos de error que no necesitan la aplicación completa, y que borrar tests sin criterio deja sin cubrir flujos críticos.
- **Respuesta senior:** Reclasificar: la validación, los errores y el contrato HTTP pasan a `@WebMvcTest` con MockMvc (milisegundos); las reglas de negocio, a tests unitarios; en REST Assured se quedan los flujos críticos completos. Reutilizar un único contenedor de base de datos para toda la suite y separar unitarios (Surefire) de integración (Failsafe) en el pipeline.


## Katas

### Kata 21.1: Contrato HTTP de tareas

Con `@WebMvcTest` y `@MockitoBean`, prueba `POST /tareas`: 201 con cabecera `Location` para un DTO válido y 400 con `ProblemDetail` (comprobando `$.title` y `$.status`) para uno sin título.

### Kata 21.2: Prueba de carga con JMeter

Crea un plan de JMeter con 100 usuarios que crean y consultan tareas durante 2 minutos. Ejecútalo con `jmeter -n` y genera el informe HTML. Anota el percentil 95, la tasa de errores y el primer recurso que se satura.


## Reglas de oro

- Valida el contrato HTTP con MockMvc; reserva REST Assured para los flujos críticos.
- En las pruebas de carga, mira percentiles y errores, no la media.
- Los escenarios BDD describen negocio, no endpoints.
- Unitarios con Surefire en cada push; integración con Failsafe en cada pull request.


## Proyecto guiado «Pedidos»: Contrato de la API y prueba de carga

Asegurar el contrato HTTP y saber cuánta carga aguanta la API.

**Tareas:**

- Prueba con MockMvc la validación y los errores de `POST /pedidos`.
- Escribe con REST Assured el flujo crear → confirmar → pagar contra la aplicación arrancada.
- Crea un plan de JMeter con 50 usuarios que crean y consultan pedidos durante 2 minutos.

**Está listo cuando:**

- `./mvnw test` ejecuta los unitarios y MockMvc; `./mvnw verify`, además, los `*IT`.
- Tienes anotados el p95, la tasa de errores y el primer recurso que se satura.


## Fuentes oficiales

- [Spring Framework: MockMvc](https://docs.spring.io/spring-framework/reference/testing/mockmvc.html)
- [REST Assured](https://rest-assured.io/)
- [Documentación de Cucumber](https://cucumber.io/docs/)
- [Manual de usuario de Apache JMeter](https://jmeter.apache.org/usermanual/index.html)

