Descripción General
Las aserciones estándar de JUnit5 se detienen en el primer fallo. Cuando verificas un objeto campo por campo, ves un error, lo corriges, vuelves a ejecutar y aparece el siguiente. Las soft assertions de AssertJ recolectan todos los fallos y los reportan juntos, así que obtienes el panorama completo en una sola ejecución.
Cuándo Usar
- Verificar un objeto con 5+ campos donde cualquier combinación puede fallar
- Probar el cuerpo de una respuesta con headers, status y payload a la vez
- Validar una transformación de datos donde todas las propiedades de salida deben cumplirse
- Quieres ciclos de depuración más rápidos: ver todos los fallos de una vez en lugar de uno por ejecución
Cuándo NO Usar
- 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 normales
- Suites con miles de aserciones donde el rendimiento es crítico: las soft assertions añaden overhead
Solución
Setup con Maven
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.7</version>
<scope>test</scope>
</dependency>
Setup con Gradle
testImplementation 'org.assertj:assertj-core:3.27.7'
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 java.time.LocalDate;
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();
}
Comparación de objetos campo a campo
@Test
void should_validate_user_dto() {
UserDto user = UserDto.builder()
.id(42)
.name("Alice")
.email("alice@example.com")
.role("admin")
.active(true)
.build();
SoftAssertions softly = new SoftAssertions();
softly.assertThat(user)
.usingRecursiveComparison()
.ignoringFields("createdAt")
.isEqualTo(expectedUser);
softly.assertAll();
}
Explicación
Cada llamada a softly.assertThat(...) pasa por un proxy que captura el AssertionError y lo guarda en un colector en lugar de lanzarlo. Nada falla hasta que se ejecuta assertAll() — entonces lanza un SoftAssertionError cuyo mensaje lista todos los fallos recolectados en el orden en que ocurrieron.
assertSoftly envuelve el mismo mecanismo en una lambda y llama assertAll() por ti en un bloque finally, así que no puedes olvidarlo. SoftAssertionsExtension conecta el colector al ciclo de vida de JUnit: todos los campos y parámetros SoftAssertions inyectados en el mismo test comparten un colector, y la extensión llama assertAll() automáticamente en afterTestExecution. Elige un mecanismo por clase de test — mezclarlos crea colectores separados y divide el reporte de fallos.
Un colector por test importa en unit testing: un campo SoftAssertions compartido entre tests filtra fallos de un test al siguiente.
Variantes
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(SoftAssertions softly) {
Result result = service.execute();
softly.assertThat(result.getCode()).isEqualTo(200);
softly.assertThat(result.getData()).isNotEmpty();
}
}
Generar soft assertions tipadas para tus clases de dominio
Escribir a mano aserciones estilo hasEmail(), hasRole() para cada clase de dominio es tedioso. El assertj-assertions-generator-maven-plugin lee tus POJOs y emite las clases de aserción más un punto de entrada SoftAssertions por ti:
<plugin>
<groupId>org.assertj</groupId>
<artifactId>assertj-assertions-generator-maven-plugin</artifactId>
<version>2.2.0</version>
<executions>
<execution>
<goals>
<goal>generate-assertions</goal>
</goals>
<configuration>
<packages>
<param>com.example.domain</param>
</packages>
</configuration>
</execution>
</executions>
</plugin>
Durante generate-test-sources, el plugin escribe UserAssert, UserSoftAssertions, Assertions y compañía bajo target/generated-test-sources/assertj-assertions para cada clase de los paquetes listados. El SoftAssertions generado extiende org.assertj.core.api.SoftAssertions y sobrecarga assertThat(User) para devolver tu UserAssert tipado:
import com.example.domain.SoftAssertions; // generado, no el de assertj-core
SoftAssertions softly = new SoftAssertions();
softly.assertThat(user)
.hasId(1)
.hasEmail("alice@example.com")
.hasRole("admin")
.isActive();
softly.assertAll();
Dos advertencias al usarlo hoy: la última versión del plugin es 2.2.0 (2019) y emite javax.annotation.Generated, que el JDK eliminó en Java 11 — agrega javax.annotation:javax.annotation-api al classpath de test o los fuentes generados no compilan. Una vez resuelto eso, las aserciones generadas funcionan bien con assertj-core actual (verificado contra 3.27.x), y el punto de entrada generado conserva todos los overloads estándar de assertThat, así que las verificaciones tipadas y built-in comparten un mismo colector.
Buenas Prácticas
- Llama
softly.assertAll()al final: sin eso, los fallos se descartan en silencio - Usa la lambda
assertSoftlypara una sintaxis más limpia cuando no necesitas reutilizar el objetoSoftAssertions - Agrega mensajes personalizados con
.as()en aserciones no obvias: aparecen en el reporte de fallos - No mezcles soft y hard assertions en el mismo test: la hard assertion se detiene antes y oculta el resto
- Mantén los bloques de soft assertions enfocados en un objeto o respuesta lógica
Para el contexto más amplio de JUnit5 — parameterized tests, extensiones, ciclo de vida — consulta la guía de testing moderno con JUnit5. Cuando tus aserciones necesitan un servicio real detrás, combínalas con tests de integración con Testcontainers.
Errores Comunes
- Olvidar
assertAll(): las soft assertions sinassertAll()siempre pasan, incluso cuando las verificaciones fallan. - Usar soft assertions para tests independientes: cada test debería verificar un comportamiento. Las soft assertions existen para un lote de verificaciones sobre la misma unidad lógica.
- Sobreusar soft assertions para checks simples: con solo 2 aserciones,
assertEqualsse lee mejor. Las soft assertions valen la pena a partir de 5+ checks. - Reutilizar un
SoftAssertionsentre tests: el colector arrastra fallos al siguiente test. Créalo por test o por bloqueassertSoftly. - No agregar mensajes descriptivos: cuando fallan 10 aserciones, necesitas contexto para saber cuál es cuál.
Solución de Problemas
- El test pasa pero las aserciones fallaron: nunca se llamó
assertAll(). Sin él, los fallos recolectados se descartan en silencio: prefiereassertSoftlyoSoftAssertionsExtensionpara que no pueda olvidarse. - Solo aparece un fallo en el reporte: un
assertThathard o una excepción dentro del bloque detiene el test antes de tiempo. Mueve esa verificación después del bloque soft, o enruta todo porsoftly. - Los fallos de aserciones personalizadas no se recolectan: las aserciones creadas con el
Assertions.assertThatestático lanzan inmediatamente. Llámalas a través desoftly.assertThat(...)o extiendeAbstractSoftAssertionspara que el punto de entrada sea proxied. @RegisterExtensionno inyecta el parámetro: el tipo del parámetro debe implementarSoftAssertionsProvider:SoftAssertionsyBDDSoftAssertionsfuncionan directamente; una clase personalizada debe extenderlo con un constructor por defecto.- El reporte es ilegible con 50+ fallos: limita los bloques soft a unas 20–30 verificaciones y divide un test grande en varios bloques
assertSoftly, uno por sección lógica.
Lectura Adicional
- Documentación de AssertJ: la referencia oficial de
SoftAssertions,assertSoftlyySoftAssertionsExtension. - Javadoc de SoftAssertions: todos los overloads de
assertThatproxied y el contrato deassertAll. - JUnit 5 User Guide:
assertAll,@RegisterExtensiony parameterized tests. - Guía completa de testing moderno con JUnit5: ciclo de vida, extensiones y organización de tests en este sitio.
- Proyecto companion ejecutable: los ejemplos de esta receta como proyecto Maven compilable.
Notas de Producción
- Mantén cada bloque soft por debajo de ~30 aserciones: el reporte agregado se vuelve difícil de leer mucho antes de que el overhead del proxy importe.
- Revisa cómo tu CI muestra
SoftAssertionError. Surefire lista cada fallo en su propia línea, pero algunos dashboards truncan el mensaje: las descripciones.as()mantienen los fallos identificables en los logs. - No excluyas los tests de soft assertions de las corridas de CI. Si son lentos, la causa suele ser miles de verificaciones proxied dentro de bucles: agrega los datos primero y aserta una sola vez.
- Las soft assertions sobre estado compartido (una base de datos de test, un stub server) siguen necesitando aislamiento por test: un registro residual hace el reporte engañoso, no solo flaky.
Puntos Clave
assertAll()es todo el mecanismo: sin él, las soft assertions pasan en silencio. PrefiereassertSoftlyoSoftAssertionsExtensionpara que sea automático.- Un colector por test: no compartas una instancia
SoftAssertionsentre tests ni mezcles el enfoque de instancia con la extensión. - Enruta cada verificación por
softly.: unassertThatestático lanza en el primer fallo y anula la recolección. - Las descripciones
.as()deciden si un reporte de 10 fallos es accionable o un muro de stack traces.
Preguntas frecuentes
¿Cuál es la diferencia entre AssertJ soft assertions y JUnit5 assertAll?
Ambos recolectan todos los fallos en lugar de detenerse en el primero. assertAll ejecuta lambdas estándar de JUnit5, así que obtienes salida plana de AssertionFailedError. Las soft assertions de AssertJ te dan la API fluida assertThat — matchers más ricos, mejores diffs y descripciones .as() en el reporte agregado.
¿Puedo usar soft assertions con aserciones personalizadas?
Sí. Extiende AbstractAssert para escribir tu propia clase de aserción, o extiende AbstractSoftAssertions para un punto de entrada soft personalizado, y luego llámalo a través del proxy softly:
softly.assertThat(user).hasValidEmail().hasActiveSubscription();
Mantén las clases personalizadas en un paquete de test compartido para que cada suite valide las mismas reglas de dominio. Si prefieres no escribirlas a mano, el assertj-assertions-generator-maven-plugin las genera desde tus POJOs — ver la variante del generador más arriba.
¿Las soft assertions funcionan con excepciones?
No. Las soft assertions recolectan fallos de aserción, no excepciones — si el código bajo test lanza, el test falla ahí mismo. Aserciona excepciones por separado con assertThatThrownBy, que es una aserción hard por diseño.
¿Cómo veo todos los fallos de soft assertions en CI?
AssertJ imprime cada fallo recolectado cuando se ejecuta assertAll() — tanto Surefire como Gradle lo reenvían al reporte de tests. El mensaje lista cada verificación fallida con su descripción .as(), razón por la que los mensajes personalizados valen la pena en los logs de CI.
¿Cómo uso soft assertions con parameterized tests?
Envuelve cada invocación en su propio bloque assertSoftly o instancia SoftAssertions — no compartas una entre parámetros, porque el colector arrastraría fallos de invocaciones anteriores. Un input que falla solo tumba su propia invocación, no todo el test.
¿Cómo combino soft assertions con matchers de Hamcrest?
Llama el overload con matcher dentro del bloque soft: softly.assertThat(actual, matcher). Eso te permite migrar una suite de Hamcrest a AssertJ gradualmente — un test a la vez en lugar de una reescritura completa.
¿Cuál es el impacto de performance de las soft assertions?
Cada verificación crea un proxy y guarda cualquier fallo en una lista; assertAll() itera esa lista una sola vez. En un test típico con una docena de verificaciones no notarás el costo. Dentro de bucles sobre miles de elementos, salta los proxies — recolecta los valores en una lista y aserta sobre ella una sola vez.
¿Puedo usar soft assertions con Kotlin?
Sí, AssertJ funciona desde Kotlin. SoftAssertions().apply { assertThat(x).isEqualTo(y) }.assertAll() hace el trabajo, pero si Kotlin es tu lenguaje principal de tests, assertAll { assert(x).isEqualTo(y) } de assertk se lee más idiomático.
¿Cómo migro de JUnit4 Assert a JUnit5 soft assertions?
Agrega assertj-core (3.27.x), reemplaza Assert.assertEquals(expected, actual) con softly.assertThat(actual).isEqualTo(expected), y termina cada test migrado con softly.assertAll(). La migración es mecánica — mantén las aserciones viejas y nuevas corriendo en paralelo durante un sprint para detectar desviaciones de comportamiento antes de borrar las originales.
¿Cómo uso soft assertions con respuestas JSON?
Deserializa a un POJO con ObjectMapper.readValue(...) cuando puedas, y luego aserta sobre los campos como cualquier otro objeto. Para JsonNode sin procesar, extrae cada campo y verifícalo a través de softly — softly.assertThat(node.get("status").asText()).isEqualTo("ok"). En arrays grandes, itera arrayNode.elements() y aserta por elemento para que un ítem malo no oculte al resto.
Recursos Relacionados
Pruebas Unitarias
Cómo escribir pruebas unitarias rápidas y deterministas con mocks y assertions en Python, JavaScript y Java.
RecipeTestcontainers para Tests de Integración en Java
Cómo usar Testcontainers en JUnit5 para levantar contenedores reales de Postgres, Redis y Kafka para tests de integración confiables y reproducibles.
RecipeStubear APIs HTTP Externos con WireMock en Java
Usá WireMock en tests de Java para stubear servicios HTTP externos. Cubre templating, simulación de delays, stubs stateful y verificación de requests.
GuideJUnit 5: Extensions, Tests Parametrizados, Dynamic Tests
Dominá JUnit 5 para testing moderno en Java: modelo de extensions, tests parametrizados, dynamic tests, test interfaces, lifecycle, conditional execution y JUnit Platform.