Skip to content

Estructura del proyecto esqueleto de Cucumber y Cliente API

Crea un proyecto de ejemplo de Cucumber en Java con la estructura de paquetes correcta, y añade un Cliente HTTP para conectar al sistema backend.

La segunda parte de esta Guía introduce el proyecto de ejemplo de la vida real de pruebas de Cucumber para scripts funcionales end-to-end. Crearás el proyecto esqueleto, diseñarás la estructura de paquetes y clases, e implementarás el HttpClient para disparar llamadas API al sistema backend. Además, ya añadiremos dos archivos de Feature de Cucumber completos usando las funcionalidades más importantes de la sintaxis Gherkin.

Un ejemplo de caso de uso real de Cucumber

Para poner Cucumber en práctica, usaremos el mismo sistema que construimos en el libro. Este proyecto tiene todos los desafíos que enfrentarías en una implementación real de pruebas end-to-end de microservicios con Cucumber, como Consistencia Eventual (que cubriremos en detalle en la Parte 4).

Describamos desde una perspectiva de alto nivel qué hace el sistema, en caso de que no hayas leído el libro (¡nunca es demasiado tarde!).

La aplicación Multiplication Challenge invita a los usuarios a resolver multiplicaciones de complejidad media usando solo cálculo mental. Es una página web donde ingresan su alias y el intento de resolver el desafío. Todos los intentos de un usuario dado (identificado por su alias) se almacenan y luego se muestran como estadísticas. Esta aplicación también integra características de Gamificación. En caso de que el intento sea correcto, el usuario obtiene su puntuación aumentada y también puede ganar insignias. Mira la Figura 1.

Aplicación Multiplication Challenge
Figura 1. La Aplicación Multiplication Challenge

En el lado técnico, la funcionalidad backend se implementa en dos microservicios: multiplication y gamification. Dado que este proyecto guía al lector a través de un ejemplo realista de una arquitectura de microservicios, incluye otros patrones comunes como un servicio Gateway (con Spring Cloud Gateway) y un registro de Service Discovery (Consul). El frontend es una aplicación React muy simple. Mira la Figura 2. Si estás interesado en los detalles técnicos, revisa el archivo README en el repositorio Github del libro (todos los códigos fuente del libro están disponibles en línea).

Arquitectura de Alto Nivel del Backend
Figura 2. Arquitectura de Alto Nivel del Backend

Desde la perspectiva de las pruebas end-to-end, interactuaremos con la API REST pública -los endpoints expuestos por el Gateway- para simular el comportamiento del usuario. No cubriremos el frontend en nuestras pruebas; este es un enfoque común en muchas organizaciones porque es un buen equilibrio entre buena cobertura end-to-end y simplicidad.

Crea un proyecto de Cucumber usando el arquetipo

Repositorio GitHub: Todo el código en este post está disponible en GitHub: Book - Cucumber Tests. ¡Si lo encuentras útil, dale una estrella!

Comencemos a codificar. Para crear un proyecto vacío en Cucumber podemos usar el arquetipo Maven (asegúrate de tener instalado Maven primero). Mira el comando completo a continuación. Puedes usar la última versión del arquetipo, que encontrarás en el tutorial de 10 minutos de Cucumber.

$ mvn archetype:generate                        \
   "-DarchetypeGroupId=io.cucumber"           \
   "-DarchetypeArtifactId=cucumber-archetype" \
   "-DarchetypeVersion=6.6.0"                 \
   "-DgroupId=microservices.book"             \
   "-DartifactId=cucumber-tests"              \
   "-Dpackage=microservices.book"             \
   "-Dversion=1.0.0-SNAPSHOT"                 \
   "-DinteractiveMode=false"

Ese comando creará una nueva carpeta llamada cucumber-tests.

El arquetipo genera un pom.xml básico que define dependencias con las últimas versiones de los artefactos cucumber-java y cucumber-junit, y añade JUnit también. Además, crea una clase para que ejecutemos las features de Cucumber con JUnit: RunCucumberTests. Mira el ejemplo a continuación. Ya describimos las anotaciones en esta clase en la primera parte de esta guía cuando cubrimos la integración Cucumber-JUnit.

package microservices.book;

import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
import org.junit.runner.RunWith;

@RunWith(Cucumber.class)
@CucumberOptions(plugin = {"pretty"})
public class RunCucumberTest {
}

El arquetipo también genera una clase StepDefinitions. Mira el código a continuación. Incluso incluye algunas cláusulas import útiles, para que podamos comenzar a añadir los métodos anotados que mapean a nuestros archivos Gherkin (.feature).

package microservices.book;

import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;

import static org.junit.Assert.*;

public class StepDefinitions {
}

Añadiendo AssertJ, deserialización JSON, Awaitility

En este proyecto de ejemplo de Cucumber, usaremos algunas bibliotecas adicionales:

  • AssertJ. Una biblioteca Java que viene con aserciones más fluidas y poderosas en comparación con JUnit. Si quieres aprender más sobre ella, guarda el post “BDD Unit Tests with AssertJ and BDDMockito” para después.
  • Jackson. Las implementaciones de (de)serialización más populares para Java. Intercambiaremos datos con las APIs REST del backend en formato JSON; esta biblioteca nos ayuda a traducir JSON desde/hacia objetos Java. Revisa el post “Java and JSON serialization with ObjectMapper” para una guía detallada sobre cómo funcionan las conversiones de Jackson.
  • Awaitility. Esta útil biblioteca nos ayuda a escribir código fluido para esperar a que ocurra una condición. Hace muy fácil crear métodos que hacen polling a un sistema hasta que algo sucede, y esto es exactamente lo que necesitamos hacer en nuestro sistema eventualmente consistente. Cubriremos esta funcionalidad en la última parte de la guía.

La versión final del archivo pom.xml con todas estas dependencias y una versión de Java más reciente es la que se muestra a continuación.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>microservices.book</groupId>
    <artifactId>cucumber-tests</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <java.version>14</java.version>
        <cucumber.version>6.6.0</cucumber.version>
        <junit.version>4.13</junit.version>
        <assertj.version>3.17.2</assertj.version>
        <jackson.version>2.11.2</jackson.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-java</artifactId>
            <version>${cucumber.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-junit</artifactId>
            <version>${cucumber.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>${assertj.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>${jackson.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.awaitility</groupId>
            <artifactId>awaitility</artifactId>
            <version>4.0.3</version>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Cómo mapear características a la sintaxis Gherkin de Cucumber

Como se describe en detalle en el libro (y se resume al principio de este post), la funcionalidad principal de nuestra aplicación es permitir a los usuarios resolver desafíos para mejorar sus habilidades cerebrales. Como adición de engagement, nuestra aplicación da puntos e insignias. En realidad, no necesito entrar en detalles sobre cómo funcionan las características del sistema porque los scripts de prueba son autoexplicativos. Son un ejemplo de cómo podemos aprovechar Cucumber y Gherkin para mostrar a las personas cómo funciona un sistema.

La característica “Solving challenges”

El primer archivo Feature que añadiremos fue parcialmente incluido en la Parte 1 como un ejemplo de Gherkin: la característica “Solving Challenges”. Mira el código a continuación. Podemos crear este archivo bajo la carpeta src/test/resources, que Cucumber escaneará. En nuestro caso, mantendremos una estructura similar al código fuente, así que lo colocaremos en la subcarpeta microservices/book.

Feature: Solve multiplication challenges
  We present users with challenges they should solve using mental calculation
  only. When they're right, we give them score and, in some cases, new badges.
  All attempts from all users are stored, so they can see their historical data.

  Scenario: Users get new attempts.
    Given a new user Mary
    When she requests a new challenge
    Then she gets a mid-complexity multiplication to solve

  Scenario: Users solve challenges, they get feedback and their stats.
    Given a new user John
    And he requests a new challenge
    When he sends the correct challenge solution
    Then his stats include 1 correct attempt

  Scenario: Users get feedback also about incorrect attempts.
    Given a new user Horatio
    When he requests a new challenge
    * he sends the incorrect challenge solution
    * he sends the correct challenge solution
    Then his stats include 1 correct attempt
    * his stats include 1 incorrect attempts

Centrémonos en el primer escenario: “Users get new attempts”. Queremos verificar si el sistema recupera un desafío de multiplicación para un Usuario nuevo. Siguiendo una sintaxis “Given-When-Then”, definimos estos pasos:

  • (Given) a new user X: esta oración define el estado del sistema, y simula un usuario que ha accedido por primera vez a la página web. Ten en cuenta que no necesitamos saber su nombre para este escenario en particular, pero estamos reutilizando este paso en otros, así que es útil mantenerlo así por consistencia.
  • (When) she (o he) requests a new challenge: esta es la acción principal realizada por el usuario. En nuestro sistema, esto se hace automáticamente por el frontend cuando el usuario accede a nuestra página web, o cuando el usuario recarga los contenidos.
  • (Then) she (o he) gets a mid-complexity multiplication to solve: lo que queremos verificar. ¿El sistema devuelve una multiplicación con dos factores entre los límites esperados? Lo afirmaremos.

En Cucumber, podemos reutilizar Definiciones de Pasos a través de escenarios y características. En este archivo Gherkin, estamos haciendo eso con los primeros dos pasos. Para que eso funcione, haremos del nombre del usuario un parámetro que se pase al método Java. Además, definiremos un texto alternativo en muchos de estos pasos, he/she, para hacer que Cucumber sea un poco más inclusivo mientras también soportamos un lenguaje natural.

El segundo y tercer escenarios introducen dos nuevos patrones de pasos:

  • he/she sends the correct/incorrect challenge solution: captura la acción del usuario cuando envía su intento. Este paso debería permitirnos enviar tanto soluciones correctas como incorrectas.
  • his/her stats include N correct/incorrect attempt(s): podemos usar este para recopilar las estadísticas del usuario actual y verificar si contienen el número esperado (N) de intentos correctos o incorrectos.

Reformulando pasos Gherkin para hacerlos reutilizables

Como se explicó en la primera parte de la guía, las definiciones de características usualmente pasan por un proceso de reformulación continua:

  1. Primero, se te ocurre algo de lenguaje natural durante la sesión donde discutes y descubres los requisitos.
  2. Luego, intentas encontrar patrones comunes en tus escenarios y los reformulas para que coincidan.
  3. La última etapa de reformulación usualmente ocurre cuando estás escribiendo las definiciones de pasos de Cucumber en código fuente. Puedes descubrir patrones extra o expresiones que podrían usarse si ajustas las oraciones Gherkin.

Por ejemplo, en mi caso, escribí originalmente el primer escenario como:

  Scenario: Users get new attempts.
    When a user accesses the application
    Then they get a mid-complexity multiplication to solve

Luego, mientras mapeaba los otros dos escenarios a código Java, noté que podía reutilizar los mismos pasos para definir un usuario y recuperar un nuevo desafío mientras mantenía este escenario legible. Este proceso de reformulación en general me permitió definir estos tres escenarios de prueba con solo cinco oraciones Gherkin diferentes.

La característica “Leaderboard”

Desde la perspectiva del usuario, la segunda característica de nuestro sistema es el leaderboard. Muestra un ranking de todos los usuarios con sus puntuaciones correspondientes. El código a continuación describe esta característica con un solo escenario.

Feature: The Leaderboard shows a ranking with all the users who solved
  challenges correctly. It displays them ordered by the highest score first.

  Scenario: Users get points and badges when solving challenges, and they
  are positioned accordingly in the Leaderboard.
    Given the following solved challenges
      | user  | solved_challenges |
      | Karen | 5                 |
      | Laura | 7                 |
    Then Karen has 50 points
    * Karen has the "First time" badge
    And Laura has 70 points
    * Laura has the "First time" badge
    * Laura has the "Bronze" badge
    And Laura is above Karen in the ranking

Nuevamente, podemos encontrar algunas oraciones en este archivo que pasaron por un proceso de reformulación para poder mapearlas a una sola definición de paso en código. Por ejemplo, (name) has N points y (name) has the (badge) badge. Para demostrar el uso de características adicionales, también añadimos aquí una Data Table de Gherkin. Estas estructuras son útiles para pasar listas de valores a nuestros scripts mientras los mantenemos legibles. Veremos cómo procesar esta tabla en la Parte 3 de esta guía.

A continuación, podríamos implementar estas definiciones de pasos con las expresiones correspondientes en código. Pero, antes de eso, analicemos cómo estructurar nuestro proyecto y los componentes que necesitamos, para que podamos evitar duplicaciones y crear un proyecto que pueda extenderse más tarde con más características fácilmente.

Repositorio GitHub: Todo el código en este post está disponible en GitHub: Book - Cucumber Tests. ¡Si lo encuentras útil, dale una estrella!

Cómo organizar la estructura de un proyecto de Cucumber

En la Parte 1, aprendimos que un proyecto de Cucumber necesita algunos elementos básicos:

  • Las definiciones de características. Estos son los scripts de prueba, escritos en Gherkin, y empaquetados en archivos .feature.
  • El punto de entrada JUnit. Una clase que hace nuestras pruebas ejecutables con JUnit, usando la anotación @RunWith. En nuestro proyecto, esto fue creado automáticamente por el arquetipo: RunCucumberTest.java.
  • Los archivos de definición de Pasos. El arquetipo también creó un ejemplo de esta clase, un solo StepDefinitions.java.

Esa es la estructura principal, pero necesitaremos más clases. Para empezar, poner toda la lógica en la misma clase de definición de pasos no es una buena idea — se convertirá en un desastre. Por lo tanto, necesitamos pensar en cómo dividir la lógica que usaremos para ejecutar los pasos.

Separaremos los componentes del proyecto siguiendo estas pautas:

  • Un paquete separado para toda la comunicación API (HTTP), y las clases Java que mapean las solicitudes y respuestas JSON (DTOs).
  • Otro paquete principal con las clases que modelan los dominios en nuestro sistema: los Challenges y el Leaderboard. Usaremos estas clases para simular interacciones y mantener el estado actual.
  • Finalmente, un tercer paquete con las clases de Definición de Pasos.

Debes evitar tener una clase de Definición de Pasos por característica de Cucumber porque eso hace más difícil reutilizar pasos a través de diferentes características y escenarios (ver Anti-patterns). En nuestro enfoque, podríamos reutilizar la clase Challenge a través de características que necesiten simular el comportamiento del usuario. Veremos esto más en detalle en la Parte 3.

La Figura 3 muestra la vista de alto nivel de nuestros componentes.

Vista de Componentes de Alto Nivel
Figura 3. Vista de Componentes de Alto Nivel

En esta guía, implementaremos todos esos componentes. Cuando terminemos el desarrollo, nuestra estructura de proyecto se verá como la mostrada a continuación (recuerda, también puedes descargar el proyecto completo desde Github).

cucumber-tests
|-- README.md
|-- mvnw
|-- mvnw.cmd
|-- pom.xml
`-- src
    `-- test
        |-- java
        |   `-- microservices
        |       `-- book
        |           |-- RunCucumberTest.java
        |           `-- cucumber
        |               |-- actors
        |               |   |-- Challenge.java
        |               |   `-- Leaderboard.java
        |               |-- api
        |               |   |-- APIClient.java
        |               |   `-- dtos
        |               |       |-- challenge
        |               |       |   |-- AttemptRequestDTO.java
        |               |       |   |-- AttemptResponseDTO.java
        |               |       |   `-- ChallengeDTO.java
        |               |       |-- leaderboard
        |               |       |   `-- LeaderboardRowDTO.java
        |               |       `-- users
        |               |           `-- UserDTO.java
        |               `-- steps
        |                   |-- ChallengeStepDefinitions.java
        |                   `-- GameStepDefinitions.java
        `-- resources
            |-- cucumber.properties
            `-- microservices
                `-- book
                    |-- leaderboard.feature
                    `-- solving_challenges.feature

Un Cliente API Java simple para pruebas de Cucumber

Para comunicarnos con nuestro backend desde las pruebas, necesitamos interactuar con la API REST expuesta a través del Gateway, ubicada en localhost:8000 cuando ejecutamos el sistema localmente. Nos conectaremos tanto a la API de Challenges como a la API de Gamification desde nuestra nueva clase APIClient.

Podríamos incluir como dependencia una de las múltiples bibliotecas que implementan un Cliente HTTP en Java, pero realmente no lo necesitamos porque podemos adherirnos a la clase HttpClient incorporada de Java, disponible en el JDK desde la versión 11.

Dado que necesitamos analizar y generar JSON, crearemos un ObjectMapper de Jackson. Lo configuraremos para ignorar propiedades desconocidas devueltas del servidor. De esta manera, podemos modelar nuestros DTOs (Data Transfer Objects) con solo los campos que necesitamos para las pruebas, y mantenemos nuestro código funcionando incluso si el backend añade nuevos atributos a la API. Las representaciones Java de la API que necesitamos crear son:

  • ChallengeDTO (ver código), que incluye la respuesta del objeto challenge del servidor.
  • AttemptRequestDTO (ver código) contiene la conjetura del usuario.
  • UserDTO (ver código) contiene la información básica del usuario: alias e identificador del servidor.
  • AttemptResponseDTO (ver código) modela la respuesta de la API de Challenges, con el resultado y el identificador del usuario.
  • LeaderboardRowDTO (ver código) representa una fila en el leaderboard. El ranking completo devuelto por el servidor puede mapearse a una lista de estos objetos.

Todos estos DTOs son objetos Java simples con un constructor básico y getters, así que no estoy incluyendo su código aquí por concisión. Revisa los enlaces en la lista si tienes curiosidad.

La lógica central de la clase APIClient usa las clases HttpClient y ObjectMapper para realizar solicitudes HTTP a la API REST del backend, mapeando JSON desde/hacia clases Java donde sea necesario. Mira el código a continuación. Por ahora, la implementación de los Body Handlers ha sido omitida. Llegaremos a eso más tarde.

public class APIClient {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    private static final String BACKEND_HOST = "http://localhost:8000";

    private final HttpClient httpClient;

    public APIClient() {
        this.httpClient = HttpClient.newHttpClient();
    }

    public HttpResponse<ChallengeDTO> getChallenge() throws Exception {
        var getRandom = HttpRequest.newBuilder(
                URI.create(BACKEND_HOST + "/challenges/random")
        ).GET().build();
        return httpClient.send(getRandom, new JsonBodyHandler<>(ChallengeDTO.class));
    }

    public HttpResponse<AttemptResponseDTO> sendAttempt(AttemptRequestDTO attempt) throws Exception {
        var sendChallenge = HttpRequest.newBuilder(
                URI.create(BACKEND_HOST + "/attempts"))
                .header("Content-Type", "application/json")
                .POST(ofString(OBJECT_MAPPER.writeValueAsString(attempt))).build();
        return httpClient.send(sendChallenge, new JsonBodyHandler<>(AttemptResponseDTO.class));
    }

    public HttpResponse<List<AttemptResponseDTO>> getStats(String user) throws Exception {
        var getStats = HttpRequest.newBuilder(
                URI.create(BACKEND_HOST + "/attempts?alias=" + user)
        ).GET().build();
        return httpClient.send(getStats, new JsonListBodyHandler<>(AttemptResponseDTO.class));
    }

    public HttpResponse<List<LeaderboardRowDTO>> getLeaderboard() throws Exception {
        var getStats = HttpRequest.newBuilder(
                URI.create(BACKEND_HOST + "/leaders")
        ).GET().build();
        return httpClient.send(getStats, new JsonListBodyHandler<>(LeaderboardRowDTO.class));
    }

    static class JsonBodyHandler<T> implements HttpResponse.BodyHandler<T> { ... }

    static class JsonListBodyHandler<T> implements HttpResponse.BodyHandler<List<T>> { ... }

}

Como ves, el estilo fluido de la clase HttpClient es sencillo de usar. Creamos solicitudes con:

var request = HttpRequest.newBuilder([target uri]).HTTP_METHOD([optional request body content]).build();

Luego, para enviar las solicitudes al servidor, llamamos:

var response = httpClient.send(request, [response body handler]);

La responsabilidad principal del body handler es leer y transformar el payload de la respuesta HTTP a la clase que especificamos. La clase BodyHandlers (desde JDK 11) incluye algunos body handlers incorporados, por ejemplo, para convertir el cuerpo de la respuesta a un objeto String (BodyHandlers.ofString()). Desafortunadamente, no hay convertidores JSON incorporados, pero no son difíciles de implementar usando el ObjectMapper de Jackson.

Construiremos dos versiones de un JSON Body Handler: uno deserializa objetos individuales y otro maneja listas. Mira el código a continuación para la implementación de la deserialización de objeto individual.

static class JsonBodyHandler<T> implements HttpResponse.BodyHandler<T> {

    private final Class<T> clazz;

    public JsonBodyHandler(Class<T> clazz) {
        this.clazz = clazz;
    }

    @Override
    public HttpResponse.BodySubscriber<T> apply(HttpResponse.ResponseInfo responseInfo) {
        var stringBodySubscriber = HttpResponse.BodySubscribers
                .ofString(StandardCharsets.UTF_8);

        return HttpResponse.BodySubscribers.mapping(
                stringBodySubscriber,
                (body) -> {
                    try {
                        return OBJECT_MAPPER.readValue(body, this.clazz);
                    } catch (JsonProcessingException e) {
                        throw new UncheckedIOException(e);
                    }
                });
    }
}

Usamos un BodySubscriber de JDK 11 para crear un subscriber que recibe un String. Cuando recibimos los datos, llamamos al ObjectMapper para deserializarlo a la clase que pasamos a este body handler. Con genéricos de Java, nos aseguramos de que esta clase sea reutilizable para todos los tipos DTO.

El otro body handler es muy similar. La diferencia principal es que devolvemos una lista de objetos, lo que requiere usar un enfoque diferente con ObjectMapper y un CollectionType. Lee el post “Java and JSON serialization with ObjectMapper” para aprender más sobre deserializar listas y otras colecciones con Jackson. Mira el código a continuación.

static class JsonListBodyHandler<T> implements HttpResponse.BodyHandler<List<T>> {

    private final CollectionType mapCollectionType;

    public JsonListBodyHandler(Class<T> clazz) {
        this.mapCollectionType = OBJECT_MAPPER.getTypeFactory()
                .constructCollectionType(List.class, clazz);
    }

    @Override
    public HttpResponse.BodySubscriber<List<T>> apply(HttpResponse.ResponseInfo responseInfo) {
        var stringBodySubscriber = HttpResponse.BodySubscribers
                .ofString(StandardCharsets.UTF_8);

        return HttpResponse.BodySubscribers.mapping(
                stringBodySubscriber,
                (body) -> {
                    try {
                        return OBJECT_MAPPER.readValue(body, this.mapCollectionType);
                    } catch (JsonProcessingException e) {
                        throw new UncheckedIOException(e);
                    }
                });
    }
}

Repositorio GitHub: Todo el código en este post está disponible en GitHub: Book - Cucumber Tests. ¡Si lo encuentras útil, dale una estrella!

Después de que terminamos la clase ApiClient, tenemos un cliente completo para nuestro servidor de aplicación que podemos usar en pruebas. Por ejemplo, si queremos recuperar un Challenge del servidor y enviar el intento correcto para resolverlo, podríamos escribir el código a continuación.

ApiClient apiClient = new ApiClient();
ChallengeDTO challenge = apiClient.getChallenge().getBody();
AttemptRequestDTO attemptRequest = new AttemptRequestDTO(
    challenge.getFactorA(),
    challenge.getFactorB(),
    "user-alias",
    challenge.getFactorA() * challenge.getFactorB()
);
AttemptResponseDTO attemptResponse = apiClient.sendAttempt(attemptRequest);
System.out.println("Is the challenge correct according to the server? " +
    attemptResponse.body().isCorrect()
);

Usaremos el ApiClient en nuestras clases de definición de pasos, pero no lo usaremos directamente desde allí. En su lugar, lo encapsularemos dentro de clases que también incluyen el estado de las diferentes interacciones que están disponibles en el sistema: el Challenge y el Leaderboard. Aprenderemos cómo lograr esto en la siguiente parte de esta guía.

Master Microservices with Spring Boot

A hands-on guide to building production-ready microservices

Get the Book

Get More Architecture Insights

Join hundreds of software architects and engineers getting practical insights delivered to their inbox.

Volver al Blog