---
name: java-testing
description: >
  Testing Java con JUnit 5, Mockito, TestContainers y AssertJ. Cubre tests
  unitarios, de integración con base de datos real y slices de Spring Boot.
  Cargar cuando se escriban tests en Java o se configure JaCoCo.
version: "1.0.0"
herramientasPermitidas: [Read, Grep]
exclusiones:
  - "No cargar para implementar el código de producción que se va a testear — primero implementar con `java-experto`, luego escribir tests."
  - "No cargar para diseñar patrones Java modernos — para diseño cargar `java-patrones`."
  - "No cargar para errores de compilación Java o Gradle en los tests — para build errors cargar `build-errors-java`."
  - "No cargar para tests Kotlin/Android — Kotlin tiene su propio ecosistema (MockK, Turbine, runTest); cargar `kotlin-testing`."
evolvable: true  # default para skill estandar
---
# Java Testing — JUnit 5, Mockito y TestContainers

Cubre el ecosistema de testing Java moderno: JUnit 5 con lifecycle y tests
parametrizados, Mockito para mocks, TestContainers para infraestructura real,
slices de Spring Boot y cobertura con JaCoCo.

## Cuándo cargar este skill

Invoca `Skill("java-testing")` cuando:

- Se escriban tests unitarios para services, domain objects o utilities Java
- Se escriban tests de integración con base de datos (PostgreSQL, MySQL, Redis)
- Se testeen controllers con MockMvc o WebTestClient
- Se configure JaCoCo para reporte de cobertura
- Se usen @SpringBootTest, @WebMvcTest o @DataJpaTest

## Cuándo NO cargar

- El código de producción aún no existe — primero implementar con `java-experto`, luego escribir tests.
- La pregunta es sobre diseño de patrones Java modernos — cargar `java-patrones`.
- Los errores son de compilación Java o Gradle en los archivos de test — cargar `build-errors-java`.
- Los tests son para un proyecto Kotlin/Android — Kotlin tiene su propio ecosistema de testing (MockK, Turbine, runTest); cargar `kotlin-testing`.

## Conceptos clave

### Slices de Spring Boot

Cada slice levanta solo la capa necesaria, acelerando los tests:
- `@WebMvcTest`: carga solo la capa web (controllers, filtros, serialization)
- `@DataJpaTest`: carga solo JPA, usa H2 por defecto o TestContainers
- `@SpringBootTest`: levanta todo el contexto; usar solo para tests E2E

### TestContainers

Lanza contenedores Docker reales durante los tests. Garantiza que el test
usa la misma versión del motor de BD que producción, eliminando diferencias
con H2 embebido.

### AssertJ

API fluida de assertions que produce mensajes de error descriptivos y permite
encadenar verificaciones sobre el mismo objeto sin repetir el subject.

## Reglas obligatorias

### Un solo comportamiento por test — nombre descriptivo

Un test verifica un solo comportamiento. El nombre documenta el escenario:
`dado_[precondicion]_cuando_[accion]_entonces_[resultado]` o su equivalente
en inglés `should_[resultado]_when_[condicion]`.

### Mockito: verificar comportamiento, no implementación

`verify()` solo para efectos secundarios observables (envío de email, publicación
de evento). No verificar cada llamada interna — eso acopla el test a la implementación.

### TestContainers con @Container estático para reutilizar

Instanciar el contenedor como `static` reutiliza el mismo contenedor en todos
los tests de la clase, evitando arranques lentos repetidos.

### AssertJ sobre assertions nativas de JUnit

`assertThat(resultado).isEqualTo(esperado)` produce mensajes de error con el
valor actual y esperado. `assertEquals(esperado, actual)` de JUnit no.

## Patrones recomendados

### Test unitario con Mockito (AAA)

```java
@ExtendWith(MockitoExtension.class)
class PedidoServiceTest {

    @Mock PedidoRepository repo;
    @Mock EventoPublisher publisher;
    @InjectMocks PedidoService service;

    @Test
    void deberiaPublicarEventoCuandoPedidoConfirmado() {
        // Arrange
        var pedido = PedidoFactory.unPedidoBorrador();
        when(repo.findById(pedido.getId())).thenReturn(Optional.of(pedido));
        when(repo.save(any())).thenAnswer(inv -> inv.getArgument(0));

        // Act
        service.confirmar(pedido.getId());

        // Assert
        var captor = ArgumentCaptor.forClass(PedidoConfirmadoEvento.class);
        verify(publisher).publicar(captor.capture());
        assertThat(captor.getValue().pedidoId()).isEqualTo(pedido.getId());
    }
}
```

### Test parametrizado con @ParameterizedTest

```java
@ParameterizedTest(name = "precio={0}, cantidad={1}, esperado={2}")
@CsvSource({
    "100.0, 2, 200.0",
    "50.5,  3, 151.5",
    "0.0,   5, 0.0"
})
void deberiaCalcularTotalCorrectamente(double precio, int cantidad, double esperado) {
    var item = new ItemPedido(UUID.randomUUID(), precio, cantidad);
    assertThat(item.total()).isEqualTo(esperado);
}
```

### @Nested para agrupar escenarios relacionados

```java
class PedidoServiceTest {

    @Nested
    class CuandoPedidoExiste {
        @Test void deberiaConfirmar() { ... }
        @Test void deberiaCancelar() { ... }
    }

    @Nested
    class CuandoPedidoNoExiste {
        @Test void deberiaLanzarNotFoundException() { ... }
    }
}
```

### TestContainers con @DataJpaTest

```java
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
@Testcontainers
class PedidoRepositoryTest {

    @Container
    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16-alpine");

    @DynamicPropertySource
    static void configurarDatasource(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired PedidoRepository repo;

    @Test
    void deberiaEncontrarPedidosPorClienteId() {
        var pedido = repo.save(PedidoFactory.unPedido());
        var resultado = repo.findByClienteId(pedido.getClienteId());
        assertThat(resultado).hasSize(1)
            .first().extracting(Pedido::getId).isEqualTo(pedido.getId());
    }
}
```

### MockMvc para tests de controller

```java
@WebMvcTest(PedidoController.class)
class PedidoControllerTest {

    @Autowired MockMvc mvc;
    @MockBean PedidoService service;

    @Test
    void deberiaRetornar201CuandoPedidoCreadoCorrectamente() throws Exception {
        var response = new PedidoResponse(UUID.randomUUID(), "Cliente X", List.of());
        when(service.crear(any())).thenReturn(response);

        mvc.perform(post("/v1/pedidos")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"clienteId": "some-uuid", "items": []}
                    """))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.id").exists());
    }
}
```

### Configuracion JaCoCo en build.gradle

```groovy
jacocoTestReport {
    dependsOn test
    reports { xml.required = true }
}

jacocoTestCoverageVerification {
    violationRules {
        rule {
            limit {
                minimum = 0.80  // 80% minimo en lineas
            }
        }
    }
}

check.dependsOn jacocoTestCoverageVerification
```

## Anti-patrones conocidos

### Thread.sleep en tests — NUNCA hacer esto

```java
// MAL — lento y no determinista
Thread.sleep(2000);
assertThat(resultado).isNotNull();

// BIEN — Awaitility para condiciones asincronas
await().atMost(5, SECONDS)
    .untilAsserted(() -> assertThat(repo.findById(id)).isPresent());
```

### @SpringBootTest cuando basta un slice

```java
// MAL — levanta todo el contexto para testear un solo controller
@SpringBootTest
@AutoConfigureMockMvc
class PedidoControllerTest { ... }

// BIEN — solo la capa web
@WebMvcTest(PedidoController.class)
class PedidoControllerTest { ... }
```

### Verify en flujos normales de lectura

```java
// MAL — verifica implementacion, no comportamiento
verify(repo, times(1)).findById(id);
verify(mapper, times(1)).toResponse(any());

// BIEN — solo verificar el resultado observable
assertThat(response.id()).isEqualTo(esperadoId);
```

## Checklist de verificación

- [ ] Cada test verifica un solo comportamiento con nombre descriptivo
- [ ] @DataJpaTest con TestContainers, no con H2 embebido
- [ ] @WebMvcTest para controllers; @SpringBootTest solo para E2E
- [ ] AssertJ en todas las assertions (no assertEquals de JUnit)
- [ ] Sin Thread.sleep: usar Awaitility para condiciones asíncronas
- [ ] JaCoCo configurado con umbral mínimo del 80%
- [ ] verify() solo para efectos secundarios, no para flujos de lectura

## Referencias

- JUnit 5 User Guide: https://junit.org/junit5/docs/current/user-guide/
- Mockito Docs: https://javadoc.io/doc/org.mockito/mockito-core/latest/
- TestContainers Java: https://java.testcontainers.org/
- AssertJ Docs: https://assertj.github.io/doc/

## Gotchas / Errores comunes no obvios

**`@MockBean` recrea el contexto de Spring en cada clase de test que lo usa**: cada clase de test con `@MockBean` distinto genera un `ApplicationContext` nuevo, aumentando el tiempo total de la suite de forma exponencial. Causa: Spring cachea el contexto solo si la configuración es idéntica entre clases. Fix: consolidar los mocks en una clase base abstracta con `@MockBean` compartida, o usar `@SpyBean` con un bean real cuando sea posible para evitar recrear el contexto.

**TestContainers con `@DynamicPropertySource` no limpia el datasource entre tests de la misma JVM**: si múltiples clases de test usan `@DynamicPropertySource` con TestContainers, los datos de un test pueden contaminar el siguiente si no se truncan las tablas en `@AfterEach`. Causa: el container reutilizado no se reinicia entre clases. Fix: usar `@Transactional` en los tests de integración (Spring lo rollbackea), o implementar `@AfterEach` que ejecuta `TRUNCATE TABLE ... CASCADE`.

**`verify(mock).metodo(...)` con `ArgumentCaptor` captura la referencia mutable, no el valor**: si el argumento capturado es una lista que se modifica después de la llamada al mock, el `captor.getValue()` refleja el estado final modificado, no el estado en el momento de la llamada. Fix: capturar y convertir a una copia inmutable inmediatamente: `List<T> captured = List.copyOf(captor.getValue())`.

**`@DataJpaTest` no carga `@Service` ni `@Component` — solo la capa JPA**: un test con `@DataJpaTest` que intenta inyectar un service o usar beans de negocio falla con `NoSuchBeanDefinitionException`. Causa: `@DataJpaTest` configura un slice mínimo con solo repositorios y el EntityManager. Fix: usar `@SpringBootTest(webEnvironment = NONE)` para tests de integración que necesiten el contexto completo, o anotar los beans adicionales con `@Import(MiService.class)`.

---
*Skill creado con swl:crear-skill el 2026-03-31. Versión 1.0.0.*
