StackPractices
intermediate Por Mathias Paulenko

JUnit5 Soft Assertions con AssertJ

Cómo usar AssertJ soft assertions en JUnit5 para recolectar múltiples fallos de aserción en un solo test en lugar de detenerse en el primer fallo.

Temas: testing

Overview

Las aserciones estándar de JUnit5 se detienen en el primer fallo. Cuando verificas un objeto con múltiples campos, ves un error y tienes que corregir y volver a ejecutar para encontrar el siguiente. AssertJ soft assertions recolectan todos los fallos y los reportan juntos, dándote el panorama completo en una sola ejecución de test.

When to Use

  • Verificar un objeto con 5+ campos donde cualquier combinación puede fallar
  • Testear un body de respuesta con headers, status y payload simultáneamente
  • Validar una transformación de datos donde múltiples propiedades de salida deben cumplirse
  • Quieres ciclos de debugging más rápidos — ver todos los fallos a la vez en lugar de uno por ejecución

When NOT to Use

  • Aserciones de un solo campo — un assertEquals estándar es más simple y claro
  • Aserciones que dependen entre sí (si A falla, B no tiene sentido) — usa aserciones regulares
  • Suites de test críticas en performance con miles de aserciones — soft assertions añaden overhead

Solution

Setup con Maven

<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.26.0</version>
    <scope>test</scope>
</dependency>

Setup con Gradle

testImplementation 'org.assertj:assertj-core:3.26.0'

Soft assertion básico

import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.Test;

class UserTest {

    @Test
    void should_validate_all_user_fields() {
        User user = userService.findById(1);

        SoftAssertions softly = new SoftAssertions();
        softly.assertThat(user.getId()).isEqualTo(1);
        softly.assertThat(user.getEmail()).isEqualTo("alice@example.com");
        softly.assertThat(user.getRole()).isEqualTo("admin");
        softly.assertThat(user.isActive()).isTrue();
        softly.assertThat(user.getCreatedAt()).isNotNull();
        softly.assertAll();
    }
}

Usando assertSoftly lambda

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.SoftAssertions.assertSoftly;

import org.junit.jupiter.api.Test;

class OrderTest {

    @Test
    void should_validate_order_response() {
        OrderResponse response = orderService.placeOrder(request);

        assertSoftly(softly -> {
            softly.assertThat(response.getStatusCode()).isEqualTo(201);
            softly.assertThat(response.getOrderId()).isNotNull();
            softly.assertThat(response.getTotal()).isEqualByComparingTo("99.99");
            softly.assertThat(response.getItems()).hasSize(3);
            softly.assertThat(response.getEstimatedDelivery()).isAfterOrEqualTo(LocalDate.now().plusDays(1));
        });
    }
}

Soft assertions en colecciones

@Test
void should_validate_all_products() {
    List<Product> products = productRepository.findAll();

    SoftAssertions softly = new SoftAssertions();
    softly.assertThat(products).hasSize(50);

    for (Product p : products) {
        softly.assertThat(p.getName()).isNotBlank();
        softly.assertThat(p.getPrice()).isPositive();
        softly.assertThat(p.getSku()).matches("^[A-Z]{3}-\\d{4}$");
    }
    softly.assertAll();
}

Mensajes de aserción personalizados

@Test
void should_validate_with_custom_messages() {
    Config config = configService.load("production");

    SoftAssertions softly = new SoftAssertions();
    softly.assertThat(config.getTimeout())
        .as("Production timeout must be at least 30s")
        .isGreaterThanOrEqualTo(30);
    softly.assertThat(config.getRetries())
        .as("Production retries must be between 1 and 5")
        .isBetween(1, 5);
    softly.assertThat(config.getFeatureFlags())
        .as("Feature flags must include 'monitoring'")
        .containsKey("monitoring");
    softly.assertAll();
}

Soft assertions con AssertJ object assertions

@Test
void should_validate_user_dto() {
    UserDto user = UserDto.builder()
        .id(42)
        .name("Alice")
        .email("alice@example.com")
        .role("admin")
        .active(true)
        .build();

    assertThat(user)
        .usingRecursiveComparison()
        .ignoringFields("createdAt")
        .isEqualTo(expectedUser);
}

Variants

JUnit5 assertAll (built-in)

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

@Test
void should_validate_with_junit5_assert_all() {
    User user = userService.findById(1);

    assertAll(
        () -> assertEquals(1, user.getId()),
        () -> assertEquals("alice@example.com", user.getEmail()),
        () -> assertTrue(user.isActive()),
        () -> assertEquals("admin", user.getRole())
    );
}

Soft assertions con @RegisterExtension

import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

class ServiceTest {

    @RegisterExtension
    final SoftAssertionsExtension soft = new SoftAssertionsExtension();

    @Test
    void should_validate(ServiceSoftAssertions softly) {
        Result result = service.execute();
        softly.assertThat(result.getCode()).isEqualTo(200);
        softly.assertThat(result.getData()).isNotEmpty();
    }
}

Best Practices

  • For a deeper guide, see Java Testcontainers Integration Tests.

  • Llama softly.assertAll() al final — sin eso, los fallos se ignoran silenciosamente

  • Usa assertSoftly lambda para sintaxis más limpia cuando no necesitas reusar el objeto SoftAssertions

  • Agrega mensajes personalizados con .as() para aserciones no obvias — aparecen en el output de fallos

  • No mezcles soft y hard assertions en el mismo test — la hard assertion se detiene antes, arruinando el propósito

  • Mantén los bloques de soft assertion enfocados en un objeto o respuesta lógica

Common Mistakes

  • Olvidar assertAll(): soft assertions sin assertAll() siempre pasan, incluso cuando las aserciones fallan.
  • Usar soft assertions para tests independientes: cada test debería verificar un comportamiento. Soft assertions son para múltiples checks en la misma unidad lógica.
  • Sobreusar soft assertions para checks simples: si tienes 2 aserciones, assertEquals regular está bien. Soft assertions brillan con 5+ checks.
  • No agregar mensajes descriptivos: cuando 10 aserciones fallan, necesitas contexto para saber cuál es cuál.

Troubleshooting

  • Flaky tests: isolate shared state, time, and randomness. Make tests independent and deterministic; quarantine persistently flaky tests.
  • High coverage but bugs in production: coverage does not guarantee correctness. Add mutation testing, property-based tests, or contract tests.
  • Slow test suite: parallelize, mock slow dependencies, and avoid end-to-end tests for logic that can be unit tested.
  • Tests pass locally but fail in CI: check environment differences, timezone, locale, and dependency versions. Pin tool versions.
  • Debugging a failing integration test: Reset state before each test.

Lectura Adicional

  • Documentación oficial: consulta la referencia actualizada del framework o herramienta utilizada.
  • Guías relacionadas: explora las guías de testing y java para profundizar.
  • Patrones complementarios: revisa los patrones de diseño aplicables a tu stack tecnológico.
  • Postmortems públicos: estudia incidentes reales de equipos que enfrentaron problemas similares en producción.

Notas de Producción

  • Despliega gradualmente usando canary o blue-green para detectar regresiones temprano.
  • Configura alertas para errores, latencia p99 y tasa de fallos antes de habilitar en producción.
  • Documenta el rollback en el runbook; prueba el procedimiento en staging al menos una vez por trimestre.
  • Revisa logs estructurados con correlation IDs para trazar requests end-to-end en incidentes.

Puntos Clave

  • Aplica junit5 soft assertions con assertj cuando necesites una solución práctica para tu caso de uso.
  • Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
  • Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
  • Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.

Errores Comunes en Producción

  • Copiar el ejemplo sin adaptarlo a volúmenes y modos de fallo reales.
  • Saltar tests de carga e inyección de errores antes del primer despliegue productivo.
  • Codificar valores fijos que deberían ser configurables por entorno.
  • Olvidar agregar logging y monitoreo en cada paso.
  • Desplegar sin plan de rollback ni estrategia de backup probada.
  • Asumir que el ejemplo mínimo escalará sin agregar caché o procesamiento por lotes.
  • No documentar la versión y configuración usadas en producción.
  • Dejar la receta sin cambios cuando evolucionan las dependencias o la escala.

Preguntas frecuentes

¿Cuál es la diferencia entre AssertJ soft assertions y JUnit5 assertAll?

Ambos recolectan múltiples fallos. assertAll usa aserciones estándar de JUnit5. AssertJ soft assertions te dan métodos fluidos (assertThat, isNotNull, matches) que son más legibles y type-safe.

¿Puedo usar soft assertions con aserciones personalizadas?

Sí. Crea una clase de aserción personalizada de AssertJ extendiendo AbstractAssert, luego úsala dentro de un bloque SoftAssertions:

softly.assertThat(user).hasValidEmail().hasActiveSubscription();
¿Las soft assertions funcionan con excepciones?

No. Soft assertions recolectan fallos de aserción, no excepciones. Si el código lanza una excepción, el test falla inmediatamente. Usa assertThatThrownBy para testing de excepciones.

¿Cómo veo todos los fallos de soft assertions en CI?

AssertJ imprime todos los fallos a standard output cuando se llama assertAll(). En CI, revisa el reporte de tests — el mensaje de fallo lista cada aserción fallida con su descripción personalizada.

¿Cómo uso soft assertions con parameterized tests?

Wrap cada invocación de parameterized test en su propio bloque SoftAssertions. No compartas una instancia de SoftAssertions entre invocaciones de parámetros — acumula fallos de todas las runs. Usa @ParameterizedTest con @MethodSource y aserta dentro de cada invocación. Si un solo parámetro falla, solo esa invocación falla, no todo el test.

¿Cómo combino soft assertions con Hamcrest matchers?

Usa assertThat(actual, matcher) dentro de un bloque SoftAssertions. SoftAssertions de AssertJ soporta Hamcrest matchers vía overloads de assertThat. Alternativamente, usa MatcherAssert.assertThat wrap en un try-catch que coleccione instancias de AssertionError. Esto te permite migrar de Hamcrest a AssertJ gradualmente sin reescribir todas las aserciones a la vez.

¿Cuál es el impacto de performance de las soft assertions?

Soft assertions agregan mínimo overhead — cada aserción es evaluada y almacenada en una lista. El call a assertAll() itera la lista y throw si alguna falló. Para 10-20 aserciones por test, el overhead es negligible. Evita soft assertions en tight loops con miles de iteraciones — usa una sola aserción aggregate en su lugar (ej., colecta results en una lista y aserta una vez).

¿Cómo agrupo soft assertions por sección lógica?

Usa múltiples bloques SoftAssertions por test, uno por sección lógica (ej., uno para validación de campos, uno para propiedades computadas). Llama assertAll() después de cada bloque para que los fallos de la primera sección se reporten antes de que la segunda run. Esto da output de test más claro y ayuda a identificar qué sección lógica falló. Alternativamente, usa SoftAssertionsProvider con assertSoftly para un scoped block.

¿Puedo usar soft assertions con Kotlin?

Sí. AssertJ funciona en Kotlin pero considera usar assertk o Kluent para sintaxis más idiomática de Kotlin. Con AssertJ en Kotlin, usa SoftAssertions().apply { softly -> softly.assertThat(x).isEqualTo(y) }.assertAll(). Para soft assertions específicas de Kotlin, assertk provee assertAll { assert(x).isEqualTo(y) } con un DSL más limpio.

¿Cómo migro de JUnit4 Assert a JUnit5 soft assertions?

Reemplaza los calls a org.junit.Assert.assertEquals con assertThat de AssertJ dentro de un bloque SoftAssertions. Agrega la dependencia AssertJ (org.assertj:assertj-core:3.25+). Reemplaza Assert.assertEquals(expected, actual) con softly.assertThat(actual).isEqualTo(expected). Agrega softly.assertAll() al final. La migración es mecánica — no se necesitan cambios en la lógica de test. Corre ambas aserciones viejas y nuevas side by side durante la migración para catchear regresiones.