Cómo Probar Streams Reactivos en Java con StepVerifier y TestPublisher
Introducción
En este tutorial, analizaremos a fondo cómo probar streams reactivos en Java utilizando StepVerifier y TestPublisher. Nos basaremos en una aplicación de Spring Reactor que contiene una cadena de operaciones reactiva. Nuestro objetivo es enseñarte a probar eficazmente los distintos escenarios que puedes encontrar al trabajar con streams reactivos en tus proyectos de Java.
1. Visión General
Los streams reactivos en Java son poderosos, pero también pueden ser complicados cuando se trata de probar su comportamiento. Aquí es donde entran en juego StepVerifier y TestPublisher. StepVerifier te permite crear pruebas paso a paso para verificar la salida de publishers reactivos como Flux
y Mono
, mientras que TestPublisher te permite emitir datos desde un publisher y simular condiciones diversas, como errores y terminaciones.
Elementos Clave
- StepVerifier: Utilizado para verificar el comportamiento de streams reactivos al definir nuestras expectativas sobre los elementos publicados y cómo se completan.
- TestPublisher: Permite crear datos programáticamente, facilitando la simulación de condiciones especiales y el manejo de errores.
2. Dependencias de Maven
Para comenzar, necesitarás agregar la dependencia reactor-test
en tu archivo pom.xml
, que contiene las clases necesarias para realizar pruebas en streams reactivos. A continuación, se muestra cómo hacerlo:
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
<version>3.6.0</version>
</dependency>
3. StepVerifier
En general, reactor-test
tiene dos usos principales:
- Crear una prueba paso a paso con StepVerifier.
- Producir datos predefinidos con TestPublisher para probar operadores downstream.
El caso más común al probar streams reactivos es aquel donde tenemos un publisher (un Flux
o Mono
) definido en nuestro código. Queremos saber cómo se comporta cuando alguien se suscribe a él.
3.1. Escenario Paso a Paso
Primero, veamos cómo crear un publisher simple con algunos operadores. Utilizaremos Flux.just(T elements)
, que crea un Flux
que emite los elementos dados y luego se completa. En este caso, filtraremos y mapearmos solo los nombres de cuatro letras a mayúsculas.
Flux<String> source = Flux.just("John", "Monica", "Mark", "Cloe", "Frank", "Casper", "Olivia", "Emily", "Cate")
.filter(name -> name.length() == 4)
.map(String::toUpperCase);
Ahora, probaremos nuestro source
con StepVerifier para verificar qué sucederá cuando alguien se suscriba:
StepVerifier
.create(source)
.expectNext("JOHN")
.expectNextMatches(name -> name.startsWith("MA"))
.expectNext("CLOE", "CATE")
.expectComplete()
.verify();
3.2. Excepciones en StepVerifier
Imaginemos que concatenamos nuestro publisher Flux
con un Mono
que termina inmediatamente con un error. A continuación, ejecutamos la prueba para esperar que termine con la excepción.
Flux<String> error = source.concatWith(
Mono.error(new IllegalArgumentException("Nuestro mensaje"))
);
StepVerifier
.create(error)
.expectNextCount(4)
.expectErrorMatches(throwable -> throwable instanceof IllegalArgumentException &&
throwable.getMessage().equals("Nuestro mensaje"))
.verify();
3.3. Probando Publishers Basados en Tiempo
Si manejas publishers que dependen de un tiempo, como un retraso de un día entre eventos, sería muy ineficiente ejecutar la prueba durante un día real. Para esto, usamos StepVerifier.withVirtualTime
, que nos permite realizar pruebas sin esperar el tiempo real.
StepVerifier
.withVirtualTime(() -> Flux.interval(Duration.ofSeconds(1)).take(2))
.expectSubscription()
.expectNoEvent(Duration.ofSeconds(1))
.expectNext(0L)
.thenAwait(Duration.ofSeconds(1))
.expectNext(1L)
.verifyComplete();
Aquí, se utilizan dos métodos de expectativas que manejan el tiempo: thenAwait(Duration duration)
y expectNoEvent(Duration duration)
.
3.4. Aserciones Post-Ejecución
A veces, puede ser necesario verificar el estado adicional después de que todo el escenario se haya ejecutado correctamente. Por ejemplo, podemos querer comprobar elementos que fueron eliminados. He aquí un ejemplo de cómo crear un publisher personalizado para demostrar esto:
Flux<Integer> source = Flux.create(emitter -> {
emitter.next(1);
emitter.next(2);
emitter.next(3);
emitter.complete();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
emitter.next(4);
}).filter(number -> number % 2 == 0);
@Test
public void droppedElements() {
StepVerifier.create(source)
.expectNext(2)
.expectComplete()
.verifyThenAssertThat()
.hasDropped(4)
.tookLessThan(Duration.ofMillis(1050));
}
4. Produciendo Datos con TestPublisher
A veces, necesitamos datos especiales para activar señales según lo necesitado. Aquí es donde TestPublisher<T> se vuelve útil, ya que permite emitir señales al suscriptor y simular diversas condiciones, como errores o completaciones.
4.1. Creando un TestPublisher
Veamos cómo implementar un simple TestPublisher que emita algunas señales y luego terminaremos con una excepción:
TestPublisher
.<String>create()
.next("Primero", "Segundo", "Tercero")
.error(new RuntimeException("Mensaje"));
4.2. TestPublisher en Acción
Para demostrar el uso de TestPublisher, crearemos una clase que use Flux<String>
como parámetro de constructor para realizar la operación getUpperCase()
:
class UppercaseConverter {
private final Flux<String> source;
UppercaseConverter(Flux<String> source) {
this.source = source;
}
Flux<String> getUpperCase() {
return source.map(String::toUpperCase);
}
}
Ahora, al instanciar el UppercaseConverter
, podemos utilizar TestPublisher para emitir valores específicos.
final TestPublisher<String> testPublisher = TestPublisher.create();
UppercaseConverter uppercaseConverter = new UppercaseConverter(testPublisher.flux());
StepVerifier.create(uppercaseConverter.getUpperCase())
.then(() -> testPublisher.emit("aA", "bb", "ccc"))
.expectNext("AA", "BB", "CCC")
.verifyComplete();
4.3. TestPublisher Incorrecto
A veces, podrás crear un TestPublisher que no cumpla con las especificaciones del estándar mediante el método de fábrica createNonCompliant
. Aquí tenemos un ejemplo de un TestPublisher que permite elementos nulos:
TestPublisher
.createNoncompliant(TestPublisher.Violation.ALLOW_NULL)
.emit("1", "2", null, "3");
5. Conclusión
En este artículo, discutimos diversas maneras de probar streams reactivos del proyecto Spring Reactor. Primero, vimos cómo usar StepVerifier para probar publishers, seguido de TestPublisher. También abordamos cómo manejar publishers incorrectos para validar el comportamiento en situaciones inusuales.
Dominar estas herramientas te permitirá mejorar la calidad de tu código y garantizar un manejo adecuado de los streams reactivos en tus aplicaciones Java. Recuerda que las pruebas son una parte esencial del desarrollo; invertir tiempo en ellas te proporcionará mayores beneficios en el futuro.
¡Ahora es tu turno! Comienza a experimentar con StepVerifier y TestPublisher y mejora tus habilidades en pruebas de streams reactivos en Java.