Skip to content
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

Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.

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.

FAQ

¿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.

¿Cómo uso soft assertions con collections?

AssertJ provee aserciones collection-specific dentro de SoftAssertions. Usa softly.assertThat(list).hasSize(3).containsExactly("a", "b", "c") para chequear múltiples propiedades de collection en un bloque. Para collections anidadas, usa softly.assertThat(list).flatExtracting(Item::getTags).contains("tag1"). Para collections grandes, evita containsExactly con soft assertions — si un elemento difiere, el mensaje de aserción se vuelve unwieldy. Usa assertThat con contains y hasSize separadamente para mensajes de fallo más claros.

¿Cómo uso soft assertions con streams y código reactive?

Para aserciones de Stream y Flux, colecta el stream a una lista primero, luego aserta sobre la lista colectada. No asertes sobre un stream abierto — las terminal operations lo consumen. Usa softly.assertThat(stream.toList()).hasSize(3) con AssertJ. Para streams reactivos (Project Reactor), usa StepVerifier con expectNext y expectComplete — estas no son soft assertions, así que wrap múltiples calls de StepVerifier en métodos de test separados si necesitas reporting de fallo independiente.

¿Cómo uso soft assertions con respuestas JSON?

Usa assertThat(jsonNode).hasFieldOrProperty("name") de AssertJ dentro de un bloque SoftAssertions. Para JsonNode de Jackson, extrae campos con node.get("field").asText() y aserta en cada uno. Para deserialización a POJO, usa ObjectMapper.readValue(json, MyClass.class) y luego aserta sobre el objeto deserializado. AssertJ Guava provee assertThat(jsonNode) con aserciones JSON-specific si agregas la dependencia assertj-guava. Para JSON arrays, usa softly.assertThat(arrayNode).hasSize(3) e itera con arrayNode.elements() para asertar en cada elemento individualmente.

¿Cómo uso soft assertions con clases de aserción custom?

Extiende AbstractSoftAssertions para crear clases de soft assertion custom. Implementa assertThat(MyType actual) retornando una aserción custom que extienda AbstractAssert. Registra la clase con SoftAssertionsProvider o usa SoftAssertions.assertSoftly(softly -> softly.assertThat(myObject).hasValidState()). Las aserciones custom encapsulan lógica de validación domain-specific (ej., assertThat(user).isActive().hasValidEmail()) y hacen el intent del test más claro que aserciones field-by-field raw. Reusa aserciones custom across test classes para reducir duplicación y mantener consistencia cuando las reglas de dominio cambian. Genera clases de aserción custom con el generator maven plugin de AssertJ para aserciones boilerplate-free desde tus POJOs.

¿Cómo deshabilito soft assertions en tests de producción?

Usa la anotación @Disabled de JUnit5 en clases de test que contienen soft assertions si necesitas skipearlas temporalmente. Para deshabilitado condicional, usa @EnabledIfEnvironmentVariable para correr soft assertion tests solo en CI. No wrap softly.assertAll() en try-catch para suprimir failures — esto derrota el propósito. Si las soft assertions son muy lentas en CI, sepáralas en un test suite separado taggeado con @Tag("soft") y córrelas con la configuración groups de Maven Surefire.