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.
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
assertEqualsestá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
assertSoftlylambda para sintaxis más limpia cuando no necesitas reusar el objetoSoftAssertions -
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 sinassertAll()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,
assertEqualsregular 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.
Recursos Relacionados
Unit Testing
How to write fast, deterministic unit tests with mocks and assertions in Python, JavaScript, and Java.
RecipeJava Testcontainers Integration Tests
How to use Testcontainers in JUnit5 to spin up real Postgres, Redis, and Kafka containers for integration tests that are reliable and reproducible.
RecipeStub External HTTP Services with WireMock
How to use WireMock in Java tests to stub external HTTP services, including response templating, delay simulation, and stateful mock behavior.
GuideJUnit 5: Extensions, Parameterized Tests, Dynamic Tests
Master JUnit 5 for modern Java testing: extensions model, parameterized tests, dynamic tests, test interfaces, lifecycle, conditional execution, and JUnit Platform integration.