# Regla: Testing Java

Los tests no son opcionales. Un método sin test es un método que no se puede
modificar con confianza. Esta regla aplica a todo código Java nuevo o modificado.

---

## JUnit 5 como framework principal

JUnit 5 (Jupiter) es el único framework de testing aceptado. JUnit 4 está prohibido
en código nuevo. No mezclar anotaciones de ambas versiones en el mismo proyecto.

```java
// MAL — JUnit 4 en código nuevo
import org.junit.Test;
import org.junit.Before;

// BIEN — JUnit 5
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
```

---

## @DisplayName descriptivos en español

Los nombres de los métodos de test son para máquinas. `@DisplayName` es para humanos.
Todo test DEBE tener `@DisplayName` con el escenario completo en español.

```java
// MAL — nombre técnico sin contexto
@Test
void test1() { ... }

@Test
void calcularIvaTest() { ... }

// BIEN — describe el escenario y el resultado esperado
@Test
@DisplayName("Calcula IVA de 16% para productos gravados con tasa estándar")
void calcularIva_productoGravadoTasaEstandar_retornaIva16Porciento() {
    // ...
}

@Test
@DisplayName("Lanza excepción cuando el subtotal es negativo")
void calcularIva_subtotalNegativo_lanzaIllegalArgumentException() {
    // ...
}
```

---

## Patrón Arrange-Act-Assert (AAA)

Todos los tests siguen AAA. Los tres bloques separados con comentario o línea en blanco.

```java
@Test
@DisplayName("Crea factura con total correcto para múltiples líneas con IVA")
void crearFactura_variasLineas_calculaTotalConIva() {
    // Arrange
    var cliente = ClienteFactory.clienteActivo();
    var lineas = List.of(
        LineaFactura.de(Producto.con(precio("100.00")), cantidad(2)),
        LineaFactura.de(Producto.con(precio("50.00")), cantidad(1))
    );
    var servicio = new FacturaService(repositorioMock, calculadorIva);

    // Act
    var factura = servicio.crear(cliente, lineas);

    // Assert
    assertThat(factura.getSubtotal()).isEqualByComparingTo("250.00");
    assertThat(factura.getIva()).isEqualByComparingTo("40.00");
    assertThat(factura.getTotal()).isEqualByComparingTo("290.00");
}
```

---

## Mockito para mocks — no PowerMock

PowerMock no se usa. Si el código requiere PowerMock (mockear static, final,
constructores), el código tiene un problema de diseño que debe corregirse primero.

```java
// Configuración estándar
@ExtendWith(MockitoExtension.class)
class FacturaServiceTest {
    @Mock
    private FacturaRepository repositorio;

    @Mock
    private EventoPublicador publicador;

    @InjectMocks
    private FacturaService servicio;

    @Test
    @DisplayName("Publica evento cuando la factura se crea exitosamente")
    void crear_facturaValida_publicaEventoCreado() {
        // Arrange
        var factura = FacturaFactory.borradora();
        when(repositorio.guardar(any())).thenReturn(factura);

        // Act
        servicio.crear(factura.getCliente(), factura.getLineas());

        // Assert
        verify(publicador).publicar(argThat(evento ->
            evento instanceof FacturaCreada fc && fc.facturaId().equals(factura.getId())
        ));
    }
}
```

---

## AssertJ para assertions fluidas

AssertJ provee mensajes de error descriptivos y una API fluida.
Usar siempre sobre `assertEquals`, `assertTrue` y similares de JUnit.

```java
// MAL — assertions básicas con mensajes de error pobres
assertEquals("ACTIVA", factura.getEstatus());
assertTrue(factura.getLineas().size() > 0);
assertNotNull(factura.getId());

// BIEN — AssertJ: legible y con mensajes de error útiles al fallar
assertThat(factura.getEstatus()).isEqualTo(EstatusFactura.ACTIVA);
assertThat(factura.getLineas()).isNotEmpty().hasSize(3);
assertThat(factura.getId()).isNotNull();
assertThat(factura.getTotal()).isGreaterThan(BigDecimal.ZERO);

// Para excepciones
assertThatThrownBy(() -> servicio.crear(clienteInactivo, lineas))
    .isInstanceOf(ClienteInactivoException.class)
    .hasMessageContaining("inactivo");
```

---

## TestContainers para tests de integración con BD

Las pruebas de integración usan TestContainers. No hay bases de datos H2 en
tests — H2 tiene comportamiento diferente a PostgreSQL/MySQL en producción.

```java
@SpringBootTest
@Testcontainers
class FacturaRepositoryIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
        .withDatabaseName("test_db")
        .withUsername("test")
        .withPassword("test");

    @DynamicPropertySource
    static void configurarPropiedades(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Test
    @DisplayName("Persiste y recupera factura con todas sus líneas")
    void guardar_facturaConLineas_persisteCorrectamente() { ... }
}
```

---

## Tests parametrizados con @ParameterizedTest

Para múltiples casos del mismo escenario, usar `@ParameterizedTest` en lugar
de duplicar métodos de test.

```java
@ParameterizedTest(name = "Tasa {0}% sobre ${1} resulta en ${2} de IVA")
@CsvSource({
    "0.16, 100.00, 16.00",
    "0.08, 100.00, 8.00",
    "0.00, 100.00, 0.00",
    "0.16, 0.00,   0.00"
})
@DisplayName("Calcula IVA correctamente para diferentes tasas y subtotales")
void calcularIva_variasConfiguraciones_resultadoCorrecto(
        double tasa, String subtotal, String ivaEsperado) {
    var resultado = calculador.calcular(new BigDecimal(subtotal), tasa);
    assertThat(resultado).isEqualByComparingTo(ivaEsperado);
}
```

---

## Cobertura mínima 80% con JaCoCo

JaCoCo en CI con umbral de fallo. Sin excepciones para módulos de lógica de negocio.

```xml
<!-- pom.xml -->
<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <configuration>
    <rules>
      <rule>
        <element>BUNDLE</element>
        <limits>
          <limit>
            <counter>LINE</counter>
            <value>COVEREDRATIO</value>
            <minimum>0.80</minimum>
          </limit>
        </limits>
      </rule>
    </rules>
  </configuration>
</plugin>
```

---

## Checklist de testing Java antes de abrir PR

- [ ] Todo método público nuevo tiene al menos un test
- [ ] Tests usan @DisplayName descriptivo en español
- [ ] Patrón AAA aplicado con separación visual clara
- [ ] Mocks con Mockito — sin PowerMock
- [ ] Assertions con AssertJ — sin assertEquals de JUnit
- [ ] Tests de integración con TestContainers (no H2)
- [ ] Casos parametrizados usan @ParameterizedTest
- [ ] Cobertura del módulo >= 80% en reporte JaCoCo
- [ ] Sin Thread.sleep() en tests
