Skip to content

Usando Awaitility con Cucumber para verificaciones de Eventual Consistency

Escribe pruebas end-to-end estables de Cucumber con Awaitility para lidiar con sistemas asíncronos y eventualmente consistentes.

La última parte de la guía se centra en construir pruebas end-to-end con Cucumber que soporten eventual consistency. Usamos la segunda característica, el Leaderboard, para mostrar cómo integrar Awaitility en pruebas de Cucumber con un ejemplo práctico.

Eventual Consistency y Pruebas de Cucumber

Strong Consistency en Pruebas

Las Step Definitions de Cucumber que escribimos en la Parte 3 asumieron strong consistency en los datos del backend. Justo después de que los usuarios envían sus intentos para resolver los desafíos de multiplicación, recuperamos las estadísticas para verificarlas, y revisamos cómo incluyen el último intento. Mira el código a continuación con el fragmento Gherkin mostrando la parte relevante de esta prueba.

When he sends the correct challenge solution
Then his stats include 1 correct attempt

El código a continuación muestra la implementación Java de la step definition para verificar las estadísticas.

@Then("her/his stats include {int} {correct} attempt(s)")
public void statsIncludeAttempts(int attemptNumber, boolean correct) throws Exception {
    var stats = this.challengeActor.retrieveStats();
    assertThat(stats)
            .filteredOn("correct", true)
            .hasSize(attemptNumber);
}

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

Esto es muy intuitivo y directo porque los sistemas que usan strong consistency son fáciles de probar. Sin embargo, el caso de uso práctico que construimos en el libro no es fuertemente consistente (y dedicamos un capítulo completo a analizar por qué no lo es).

Desafíos de Eventual Consistency con Cucumber

Nuestra arquitectura backend está usando microservicios y eventual consistency. Mira la Figura 1. Si leíste el libro, esta figura debería ser familiar para ti.

Eventual Consistency en nuestro sistema
Figura 1. Eventual Consistency en nuestro sistema

Cuando un usuario envía un intento al backend (1), ese intento se verifica y almacena en la base de datos antes de devolver una respuesta (3). Por lo tanto, cuando más tarde pedimos las estadísticas, el último intento está incluido allí. Estas dos operaciones están bajo el alcance del microservicio Multiplication.

En el libro, explicamos cómo logramos acoplamiento débil usando un enfoque dirigido por eventos: en lugar de hacer que el dominio challenge conozca el dominio gamification, el primer dominio dispara un evento (2) vía un message broker (RabbitMQ) cuando se procesa un intento. La lógica de gamification usa datos del evento para calcular el puntaje y las insignias de los usuarios, pero esta operación (4) puede suceder después de que la respuesta al desafío ha sido enviada (3).

Si no abrazamos la eventual consistency en nuestras pruebas, podríamos terminar construyendo pruebas inestables que a veces pasan y a veces fallan. Veamos de nuevo una parte de nuestra segunda definición de característica en Gherkin. Mira el código a continuación.

Given the following solved challenges
  | user  | solved_challenges |
  | Karen | 5                 |
  | Laura | 7                 |
Then Karen has 50 points
* Karen has the "First time" badge

Para recuperar el leaderboard, usaremos el REST API expuesto por el microservicio Gamification, aunque esto está abstraído de nosotros gracias al patrón Gateway. Si no recuerdas la arquitectura backend completa, echa un vistazo de nuevo a la Figura 2 en la Parte 2 de esta guía. De los datos del leaderboard, extraeremos el puntaje y las insignias.

Lo que podría suceder es que la parte Then de nuestro script de prueba llame al API de Gamification antes de que este microservicio haya recibido y procesado todos los mensajes de RabbitMQ que llevan esos intentos (como se muestra en la Figura 1). Karen podría tener 0 puntos, o 20, o quizás 50 si tenemos suerte. Nadie sabe porque depende de los entornos donde las pruebas y el sistema están corriendo.

Para una mejor comprensión del problema, lo demostraremos primero. Luego, pasaremos por las alternativas y las mejores prácticas que puedes usar para lidiar con la eventual consistency en pruebas de Cucumber.

La clase GameStepDefinitions

Ya incluimos en nuestro proyecto el script de la característica Leaderboard cuando describimos las características Gherkin, pero echemos un segundo vistazo. Mira el código a continuación.

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

Ahora que tenemos experiencia con archivos de Step definition y preparamos la clase actor Leaderboard, podemos crear una primera versión de la clase GameStepDefinitions. Mira el código a continuación.

package microservices.book.cucumber.steps;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import io.cucumber.datatable.DataTable;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;

import microservices.book.cucumber.actors.Challenge;
import microservices.book.cucumber.actors.Leaderboard;
import microservices.book.cucumber.api.dtos.leaderboard.LeaderboardRowDTO;

import static org.assertj.core.api.Assertions.*;

public class GameStepDefinitions {

    private Map<String, Challenge> userActors;
    private final Leaderboard leaderboardActor;

    public GameStepDefinitions() {
        this.leaderboardActor = new Leaderboard();
    }

    @Given("the following solved challenges")
    public void theFollowingSolvedChallenges(DataTable dataTable) throws Exception {
        processSolvedChallenges(dataTable);
    }

    private void processSolvedChallenges(DataTable userToSolvedChallenges) throws Exception {
        userActors = new HashMap<>();
        for (var userToSolved : userToSolvedChallenges.asMaps()) {
            var user = new Challenge(userToSolved.get("user"));
            user.askForChallenge();
            int solved = Integer.parseInt(userToSolved.get("solved_challenges"));
            for (int i = 0; i < solved; i++) {
                user.solveChallenge(true);
            }
            userActors.put(user.getOriginalName(), user);
        }
    }

    @Then("{word} has {int} points")
    public void userHasPoints(String user, long score) throws Exception {
        Optional<LeaderboardRowDTO> optionalRow = this.leaderboardActor
                .update()
                .getByUserId(userActors.get(user).getUserId());
        assertThat(optionalRow).isPresent()
                .map(LeaderboardRowDTO::getTotalScore).hasValue(score);
    }

    @Then("{word} has the {string} badge")
    public void userHasBadge(String user, String badge) throws Exception {
        Optional<LeaderboardRowDTO> optionalRow = this.leaderboardActor
                .update()
                .getByUserId(userActors.get(user).getUserId());
        assertThat(optionalRow).isPresent();
        assertThat(optionalRow.get().getBadges()).contains(badge);
    }

    @Then("{word} is above {word} in the ranking")
    public void userIsAboveUser(String userAbove, String userBelow) throws Exception {
        var updatedLeaderboard = this.leaderboardActor.update();
        int positionAbove = updatedLeaderboard.whatPosition(
                userActors.get(userAbove).getUserId()
        );
        int positionBelow = updatedLeaderboard.whatPosition(
                userActors.get(userBelow).getUserId()
        );
        assertThat(positionAbove).isLessThan(positionBelow);
    }

}

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

Este código sigue un enfoque similar a nuestras implementaciones de pasos anteriores. Usamos la instancia leaderboardActor para mantener el estado entre pasos (como se introdujo en la Parte 3). Además, necesitamos simular varios usuarios enviando intentos al sistema, y para eso, aprovechamos la clase UserActor que ya creamos. Es ahora cuando realmente vemos las ventajas de la capa de abstracción de actor: no necesitamos replicar en esta clase de step definition todas las variables de estado e interacciones.

Excepto por la primera step definition, que usa un Datatable de Cucumber, todas las otras son simples:

  • El método userHasPoints se encarga de verificar si un usuario dado en el leaderboard tiene el puntaje esperado. Actualiza el leaderboard e intenta encontrar al usuario. Si está allí, comparará el puntaje real con el valor esperado. Solo si coincide, este paso pasará.
  • El método userHasBadge hace lo mismo pero para insignias esperadas. Nota que es la primera vez que usamos el tipo de parámetro de Cucumber {string}, porque las insignias pueden consistir en varias palabras. Por la misma razón, encerramos el nombre de la insignia entre comillas en Gherkin (por ejemplo, “First time”).
  • userIsAboveUser toma dos nombres de usuario como parámetros y verifica que uno esté por encima del otro en el ranking. Ten en cuenta que no podemos verificar posiciones absolutas porque creamos usuarios aleatorios en otros escenarios de prueba, así que nunca sabemos qué otros usuarios ya están en el ranking. Por lo tanto, usamos una comparación relativa.

Cubramos Datatables en una sección separada.

Datatables de Cucumber en la práctica

En Cucumber, podemos pasar estructuras de datos a nuestras pruebas. Esto es muy conveniente porque evitamos la repetición de pasos en Gherkin, lo que mejora la legibilidad. Para nuestros casos de prueba, podemos definir rápidamente precondiciones para múltiples usuarios de una manera más visual. Mira el código a continuación, un extracto de la característica Leaderboard.

    Given the following solved challenges
      | user  | solved_challenges |
      | Karen | 5                 |
      | Laura | 7                 |

Como puedes imaginar, podríamos incluir más filas en otros casos de prueba. Dado que estamos probando el ranking, esta sintaxis nos ayuda a visualizar que Laura debería estar realmente en una posición de ranking más alta que Karen.

En el código, leemos el Datatable simplemente declarándolo como un argumento del método. No necesitamos especificar nada extra.

La clase Datatable ofrece múltiples métodos para leer la estructura de múltiples maneras diferentes:

  • Como una lista de listas: una lista de filas con cada fila teniendo múltiples valores.
  • Como un map: si la tabla tiene dos columnas, la primera será la clave, y la segunda el valor.
  • Como una lista de maps: cada elemento en la lista es un map de una sola entrada cuyas claves son los encabezados de la tabla y los valores son las entradas en esa fila particular. En mi opinión, esta es la forma más intuitiva de leer el Datatable porque usa los encabezados de la tabla.
  • etc. Revisa los documentos oficiales si tienes curiosidad.

El código a continuación es un extracto del código completo de la clase GameStepDefinitions. En este fragmento, vemos cómo recorremos todas las filas devueltas por el método .asMaps() y usamos los encabezados de la tabla para obtener el nombre de usuario, y el número de desafíos resueltos (usando Map.get() para cada fila usertoSolved). Para lograr nuestro objetivo, enviamos tantos intentos como se especifiquen para el usuario simulado vía la clase actor (user.solveChallenge()).

private void processSolvedChallenges(DataTable userToSolvedChallenges) throws Exception {
    userActors = new HashMap<>();
    for (var userToSolved : userToSolvedChallenges.asMaps()) {
        var user = new Challenge(userToSolved.get("user"));
        user.askForChallenge();
        int solved = Integer.parseInt(userToSolved.get("solved_challenges"));
        for (int i = 0; i < solved; i++) {
            user.solveChallenge(true);
        }
        userActors.put(user.getOriginalName(), user);
    }
}

Ejecutando las pruebas

Después de que implementamos las step definitions faltantes, ahora podemos ejecutar las pruebas de nuevo y revisar los resultados. Aún no preparamos nada para soportar eventual consistency, así que esperamos que estas pruebas sean inestables.

Antes de ejecutar las pruebas, debes iniciar el sistema backend. Como introdujimos en la Parte 3, la forma más fácil de hacerlo es descargar el archivo docker-compose-public.yml de los repositorios del libro, y luego ejecutarlo usando Docker Compose.

$ docker-compose -f docker-compose-public.yml up

Después de que todos los servicios inicien, puedes ejecutar la suite de pruebas con:

$ ./mvnw clean test

Si tienes suerte, todas las pruebas pasarán. El escenario de la característica Leaderboard será marcado en verde, y Maven te dará un resultado de cero fallas. Puedes intentar múltiples veces y obtener el mismo resultado: todo pasa.

¿Qué sucede aquí? ¿Inventé el desafío de eventual consistency? No realmente. Significa que el sistema backend es lo suficientemente rápido para producir, enviar, consumir y procesar todos los mensajes de RabbitMQ antes de que la prueba llame al API del leaderboard. Si miras de nuevo la Figura 1 anterior, significa que (3 - enviar mensaje) y (4 - procesar evento) se completan antes de la llamada al API. Los resultados de las pruebas dependen mucho del entorno donde las estás ejecutando, así que incluso cuando obtengas que todas pasen, no deberías relajarte y pensar que son lo suficientemente estables. No sigas el lema “Funciona en mi máquina”. Mañana, un colega podría experimentar errores al ejecutarlas en una computadora diferente, o quizás tu sistema CI/CD tenga menos recursos. Entonces, verás los errores.

Forzando pruebas inestables debido a Eventual Consistency

Forcemos la situación de error para aprender mejor cómo resolverla. Lo que haremos es limitar los recursos del microservicio Gamification. Si hacemos que su contenedor funcione con CPU limitada, esperamos que sea más lento al procesar los mensajes.

Edita el archivo docker-compose-public.yml y agrega el bloque YAML deploy mostrado a continuación. Limitaremos la CPU de gamification a 0.2, lo que significa 20% de uno de los núcleos de CPU en tu máquina.

  gamification:
    image: learnmicro/gamification:0.0.1
    environment:
      - SPRING_PROFILES_ACTIVE=docker
      - SPRING_CLOUD_CONSUL_HOST=consul
    deploy:
      resources:
        limits:
          cpus: '0.20'
        reservations:
          cpus: '0.10'
    depends_on:
      - rabbitmq-dev
      - consul-importer
    networks:
      - microservices

Para que esto funcione con Docker Compose sin habilitar el modo swarm, tenemos que ejecutar esta vez el sistema con la bandera --compatibility. Recuerda bajar el sistema primero si todavía lo estás ejecutando desde la ejecución anterior.

$ docker-compose -f docker-compose-limits.yml --compatibility up

Esta vez, puede tomar un tiempo hasta que el microservicio Gamification esté completamente listo. Revisa los logs en la salida del comando docker-compose para esperar hasta que veas que el servicio está listo. En mi caso, tomó alrededor de tres minutos:

gamification_1     | 2020-10-09 05:30:48.084  INFO [,,,] 1 --- [           main] m.b.g.GamificationApplication            : Started GamificationApplication in 183.403 seconds (JVM running for 190.749)

Ahora, ejecuta las pruebas de nuevo para ver si podemos reproducir los errores debido a eventual consistency. En mi caso, limitar la CPU de gamification a 0.2 funciona, y obtengo a veces fallas de prueba debido a que Karen no está presente en el ranking, o tiene un puntaje más bajo de lo esperado. Mira un ejemplo de los resultados en la Figura 2, donde Karen tiene 30 puntos en lugar de los 50 esperados.

Pruebas de Cucumber fallando debido a eventual consistency
Figura 2. Pruebas de Cucumber fallando debido a eventual consistency

¿Por qué obtenemos diferentes resultados cada vez? Porque a veces la llamada al API es procesada por Gamification antes de que consumiera los mensajes, o en medio de procesarlos, o después.

Hemos reproducido el problema. ¿Cómo lo arreglamos? Veamos las alternativas.

Nota: si todavía obtienes pruebas que pasan, intenta limitar los recursos de CPU aún más.

El enfoque Thread.sleep() en Cucumber

Una conclusión simple que podemos extraer de nuestro experimento es que las pruebas que hicimos son demasiado rápidas. Enviamos los desafíos y, muy rápidamente después de eso, recuperamos el leaderboard.

A veces, la gente se convence de que esto nunca sucederá en un caso de uso real - una persona real interactuando con el sistema. En nuestro caso de estudio práctico, obtener los datos incorrectos del leaderboard es irrelevante porque se actualiza periódicamente. El usuario eventualmente verá el puntaje y las insignias correctos.

Por lo tanto, en muchas ocasiones, los desarrolladores introducen retrasos en las pruebas para dar algo de tiempo extra al sistema eventualmente consistente para volverse consistente. Mira el código a continuación.

@Then("{word} has {int} points")
public void userHasPoints(String user, long score) throws Exception {
    Thread.sleep(5000);
    Optional<LeaderboardRowDTO> optionalRow = this.leaderboardActor
            .update()
            .getByUserId(userActors.get(user).getUserId());
    assertThat(optionalRow).isPresent()
            .map(LeaderboardRowDTO::getTotalScore).hasValue(score);
}

Cuando esperamos esos cinco segundos extra antes de recuperar el leaderboard, el escenario de prueba pasa de nuevo. Sin embargo, esta es una mala práctica debido a múltiples razones:

  • No es eficiente. Estás desperdiciando tiempo en tus pruebas porque usualmente quieres esperar tanto como la máquina más lenta en tu organización (o sistema de nube) necesite para ejecutar las pruebas de manera estable.
  • No resuelve el problema. Mañana una nueva máquina más lenta en tu organización podría requerir más de cinco segundos para ver resultados estables. Aún peor, todos los otros sistemas tienen que esperar por el nuevo tiempo de guardia más largo.
  • Hace que tus pruebas sean más difíciles de mantener. En nuestro ejemplo, esperamos cada vez que revisamos el puntaje. Sin embargo, no necesitamos esperar la segunda vez. Así que, podríamos terminar con flujos realmente complicados aquí si intentamos optimizarlo, que incluso pueden depender de cómo escribimos los escenarios Gherkin.

Afortunadamente, hay una mejor manera de introducir estas guardias de tiempo y mantener las pruebas legibles y eficientes: usando una biblioteca de polling como Awaitility.

Un ejemplo práctico de Awaitility y Cucumber

Awaitility es una biblioteca Java simple para probar sistemas asíncronos. En pocas palabras, reintenta llamadas a una “función de condición” hasta que la condición se cumple o un timeout expira. Lo hace de una manera legible, así que nuestras step definitions de prueba siguen siendo fáciles de entender.

En nuestro caso, podemos usar Awaitility para sondear el sistema backend hasta que la aserción para el resultado esperado pase. También definiremos un período máximo de polling de 5 segundos, pero podríamos modificarlo fácilmente cuando sea necesario. Mira el código a continuación para la nueva implementación del método userHasPoints usando Awaitility.

@Then("{word} has {int} points")
public void userHasPoints(String user, long score) {
    await().atMost(5, TimeUnit.SECONDS).untilAsserted(
            () -> {
                Optional<LeaderboardRowDTO> optionalRow = this.leaderboardActor
                        .update()
                        .getByUserId(userActors.get(user).getUserId());
                assertThat(optionalRow).isPresent()
                        .map(LeaderboardRowDTO::getTotalScore).hasValue(score);
            }
    );
}

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

Para usar las opciones de la biblioteca proporcionadas por Awaitility, llamamos a su método estático await(). Hay múltiples opciones y funciones que puedes usar, como se describe en su documentación de Uso. En nuestro ejemplo, lo configuramos para sondear durante un máximo de 5 segundos con atMost(). Pasamos una función como lambda vía untilAsserted(), que podemos usar para instruir a Awaitility a esperar hasta que la aserción de la función pase. Esta es una buena manera de combinar Awaitility con AssertJ. Si no quieres usar AssertJ, también podrías usar el método genérico until(), que espera una función Callable<Boolean> o lambda que será llamada hasta que devuelva true. También acepta matchers de Hamcrest, revisa los docs para más detalles.

Podemos modificar todos los pasos en GameStepDefinitions que dependen de eventual consistency para incluir Awaitility para verificar hasta que se afirmen las condiciones esperadas. Mira el código a continuación para la versión final de la clase de step definitions.

package microservices.book.cucumber.steps;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

import io.cucumber.datatable.DataTable;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;

import microservices.book.cucumber.actors.Leaderboard;
import microservices.book.cucumber.actors.Challenge;
import microservices.book.cucumber.api.dtos.leaderboard.LeaderboardRowDTO;

import static org.assertj.core.api.Assertions.*;
import static org.awaitility.Awaitility.*;

public class GameStepDefinitions {

    private Map<String, Challenge> userActors;
    private final Leaderboard leaderboardActor;

    public GameStepDefinitions() {
        this.leaderboardActor = new Leaderboard();
    }

    @Given("the following solved challenges")
    public void theFollowingSolvedChallenges(DataTable dataTable) throws Exception {
        processSolvedChallenges(dataTable);
    }

    private void processSolvedChallenges(DataTable userToSolvedChallenges) throws Exception {
        userActors = new HashMap<>();
        for (var userToSolved : userToSolvedChallenges.asMaps()) {
            var user = new Challenge(userToSolved.get("user"));
            user.askForChallenge();
            int solved = Integer.parseInt(userToSolved.get("solved_challenges"));
            for (int i = 0; i < solved; i++) {
                user.solveChallenge(true);
            }
            userActors.put(user.getOriginalName(), user);
        }
    }

    @Then("{word} has {int} points")
    public void userHasPoints(String user, long score) {
        await().atMost(5, TimeUnit.SECONDS).untilAsserted(
                () -> {
                    Optional<LeaderboardRowDTO> optionalRow = this.leaderboardActor
                            .update()
                            .getByUserId(userActors.get(user).getUserId());
                    assertThat(optionalRow).isPresent()
                            .map(LeaderboardRowDTO::getTotalScore).hasValue(score);
                }
        );
    }

    @Then("{word} has the {string} badge")
    public void userHasBadge(String user, String badge) {
        await().atMost(5, TimeUnit.SECONDS).untilAsserted(
                () -> {
                    Optional<LeaderboardRowDTO> optionalRow = this.leaderboardActor
                            .update()
                            .getByUserId(userActors.get(user).getUserId());
                    assertThat(optionalRow).isPresent();
                    assertThat(optionalRow.get().getBadges()).contains(badge);
                }
        );
    }

    @Then("{word} is above {word} in the ranking")
    public void userIsAboveUser(String userAbove, String userBelow) {
        await().atMost(5, TimeUnit.SECONDS).untilAsserted(
                () -> {
                    var updatedLeaderboard = this.leaderboardActor.update();
                    int positionAbove = updatedLeaderboard.whatPosition(
                            userActors.get(userAbove).getUserId()
                    );
                    int positionBelow = updatedLeaderboard.whatPosition(
                            userActors.get(userBelow).getUserId()
                    );
                    assertThat(positionAbove).isLessThan(positionBelow);
                }
        );
    }

}

Finalmente, hicimos nuestras pruebas estables sin importar en qué entorno se esté ejecutando solo agregando unas pocas líneas de código.

Este enfoque de polling con bibliotecas como Awaitility es una mejor manera de verificar resultados en un sistema eventualmente consistente. Intenta siempre favorecer esta técnica sobre seguir adelante sin soportar eventual consistency o usar períodos fijos.

Conclusiones y Logros

¡Alcanzamos el final de la guía! Pasamos por la especificación, diseño e implementación de una suite de pruebas de Cucumber para un sistema eventualmente consistente.

  • Aprendiste los principios de BDD y cómo habilita una mejor comunicación entre personas, para que puedas construir puentes entre la gente de negocios y los equipos de desarrollo (Parte 1).
  • Conoces lo básico sobre la sintaxis Gherkin y su estructura Given-When-Then (Parte 1).
  • Viste cómo las expresiones de Cucumber ayudan a mapear los pasos de escenario de Gherkin a código Java, usando parámetros integrados de Cucumber y personalizados (Parte 1).
  • Entendiste los componentes principales de un proyecto Java de Cucumber: los archivos de características, las clases de step definition, y el punto de entrada de JUnit (Parte 1).
  • Creaste un proyecto de Cucumber desde cero usando un ejemplo práctico de la vida real del sistema bajo prueba (Parte 2).
  • Viste algunas mejores prácticas para definir pasos Gherkin que se pueden reutilizar mientras se mantiene buena legibilidad de pruebas (Parte 2).
  • Aprendiste cómo estructurar el proyecto Cucumber en Java, y el nivel correcto de abstracción para la mejor reutilización y mantenibilidad (Parte 2).
  • Implementaste un cliente API en Java simple para interactuar con el REST API del backend (Parte 2).
  • Creaste clases Actor para mantener el estado entre pasos, que puedes reutilizar entre características (Parte 3).
  • Creaste algunos ejemplos reales de mapeo de expresiones de Cucumber a métodos Java (Parte 3).
  • Sabes cómo ejecutar pasos de Cucumber y publicar reportes en línea (Parte 3).
  • Viste cómo usar los Datatables de Cucumber (Parte 4 - este post).
  • Entendiste los desafíos de probar sistemas eventualmente consistentes con un caso de uso práctico (Parte 4 - este post).
  • Aprendiste cómo reproducir inestabilidad en las pruebas reduciendo los recursos de uno de los microservicios del backend (Parte 4 - este post).
  • Usaste Awaitility para introducir polling desde las pruebas para soportar eventual consistency (Parte 4 - este post).

Suscríbete al newsletter si quieres ser notificado cuando nuevas guías se publiquen. Si aún no tienes el libro, puedes comprar una copia en Amazon y muchas otras tiendas en línea.

AI-Assisted Dev Pack

Book, prompts and specialized agents to accelerate your coding

Get the Pack

Get More Architecture Insights

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

Volver al Blog