Serie 0x4 - Testing¶
0x4000 - El test debe tener el mismo nombre que la clase con Test al final¶
Explicación¶
Las clases de test deben seguir una convención de nombrado estricta: el nombre de la clase bajo prueba seguido del sufijo Test. Esto permite identificar rápidamente qué clase está siendo probada y facilita la navegación entre código de producción y sus tests.
Justificación¶
Navegación rápida: Los IDEs modernos permiten saltar entre clase y test con atajos de teclado.
Organización clara: Facilita localizar tests en grandes proyectos.
Convención estándar: Herramientas de cobertura y reportes esperan este formato.
Agrupación lógica: Los exploradores de tests agrupan por clase automáticamente.
Prevención de ambigüedad: Evita confusión sobre qué clase se está probando.
Sintaxis¶
<NombreClase>Test
Donde <NombreClase> es el nombre exacto de la clase bajo pruebaEjemplos correctos¶
// Clase de producción
public class Calculadora {
public int sumar(int a, int b) {
return a + b;
}
}
// Clase de test ✅
public class CalculadoraTest {
@Test
void testSumar() {
Calculadora calc = new Calculadora();
assertEquals(5, calc.sumar(2, 3));
}
}// Clase de producción
public class GestorUsuarios {
// ...
}
// Clase de test ✅
public class GestorUsuariosTest {
// ...
}// Clase de producción
public class ValidadorEmail {
// ...
}
// Clase de test ✅
public class ValidadorEmailTest {
// ...
}Ejemplos incorrectos¶
// ❌ Incorrecto - Plural
public class CalculadoraTests {
// ...
}
// ❌ Incorrecto - Prefijo en lugar de sufijo
public class TestCalculadora {
// ...
}
// ❌ Incorrecto - Nombre genérico
public class PruebasCalculadora {
// ...
}
// ❌ Incorrecto - Sin relación clara
public class PruebasMatematicas {
// Prueba Calculadora pero no es obvio
}
// ❌ Incorrecto - Abreviación
public class CalcTest {
// La clase se llama Calculadora, no Calc
}Estructura de paquetes¶
Los tests deben estar en el mismo paquete que la clase bajo prueba, pero en el directorio src/test/java:
src/
├── main/
│ └── java/
│ └── ar/
│ └── unrn/
│ └── calculo/
│ └── Calculadora.java
└── test/
└── java/
└── ar/
└── unrn/
└── calculo/
└── CalculadoraTest.java ✅Casos especiales¶
Clases internas¶
Si probás una clase interna, incluir el nombre de la clase contenedora:
// Clase de producción
public class Procesador {
public static class ResultadoProcesamiento {
// ...
}
}
// Test ✅
public class ProcesadorResultadoProcesamientoTest {
// ...
}Tests de integración¶
Para tests de integración que prueban múltiples clases, usá un sufijo más descriptivo:
// ✅ Test de integración
public class SistemaFacturacionIntegrationTest {
// Prueba la integración entre Factura, Cliente, Producto
}0x4001 - El test debe seguir la estructura AAA (Arrange-Act-Assert)¶
Explicación¶
Cada test debe organizarse en tres fases claramente diferenciadas: Arrange (preparar), Act (ejecutar) y Assert (verificar). Esta estructura proporciona un flujo lógico y predecible que facilita la comprensión y mantenimiento de los tests.
Justificación¶
Claridad: La estructura explícita hace obvio qué está siendo probado y cómo.
Mantenibilidad: Facilita modificar y extender tests existentes.
Depuración: Cuando un test falla, es fácil identificar en qué fase ocurrió el problema.
Estándar universal: Reconocida en múltiples lenguajes y frameworks.
Legibilidad: Reduce la carga cognitiva al leer tests ajenos.
Estructura¶
TEST = ARRANGE + ACT + ASSERT + [CLEANUP]
Donde:
- ARRANGE: Preparación del contexto y datos
- ACT: Ejecución de la operación bajo prueba
- ASSERT: Verificación de resultados
- CLEANUP: Limpieza opcional de recursosLas tres fases¶
Fase 1: Arrange (Preparar)¶
En esta fase se prepara todo lo necesario para ejecutar el test:
@Test
void testCalcularDescuento_ConClientePremium_AplicaDescuentoDel20Porciento() {
// Arrange: Preparar objetos y datos
Cliente cliente = new Cliente("Juan", TipoCliente.PREMIUM);
Producto producto = new Producto("Laptop", 1000.0);
CarritoCompras carrito = new CarritoCompras(cliente);
carrito.agregarProducto(producto);
// ...continúa
}Fase 2: Act (Ejecutar)¶
En esta fase se ejecuta una única acción - el método bajo prueba:
@Test
void testCalcularDescuento_ConClientePremium_AplicaDescuentoDel20Porciento() {
// Arrange
Cliente cliente = new Cliente("Juan", TipoCliente.PREMIUM);
Producto producto = new Producto("Laptop", 1000.0);
CarritoCompras carrito = new CarritoCompras(cliente);
carrito.agregarProducto(producto);
// Act: UNA llamada al método bajo prueba
double precioFinal = carrito.calcularTotal();
// ...continúa
}Fase 3: Assert (Verificar)¶
En esta fase se verifica que el resultado sea el esperado:
@Test
void testCalcularDescuento_ConClientePremium_AplicaDescuentoDel20Porciento() {
// Arrange
Cliente cliente = new Cliente("Juan", TipoCliente.PREMIUM);
Producto producto = new Producto("Laptop", 1000.0);
CarritoCompras carrito = new CarritoCompras(cliente);
carrito.agregarProducto(producto);
// Act
double precioFinal = carrito.calcularTotal();
// Assert: Verificar resultado esperado
assertEquals(800.0, precioFinal, 0.01,
"Cliente premium debe recibir 20% de descuento");
}Fase 4: Cleanup (Limpiar) - Opcional¶
Cuando el test usa recursos externos, debe limpiarlos:
@Test
void testGuardarArchivo_ConDatosValidos_CreaArchivo() throws IOException {
// Arrange
String nombreArchivo = "test_temporal.txt";
String contenido = "Datos de prueba";
GestorArchivos gestor = new GestorArchivos();
try {
// Act
gestor.guardarArchivo(nombreArchivo, contenido);
// Assert
assertTrue(new File(nombreArchivo).exists());
} finally {
// Cleanup: Eliminar archivo de prueba
new File(nombreArchivo).delete();
}
}Ejemplos completos¶
Test simple con las tres fases¶
public class CalculadoraTest {
@Test
void testSumar_ConDosNumerosPositivos_RetornaSuma() {
// Arrange: Preparar calculadora y datos de entrada
Calculadora calc = new Calculadora();
int operando1 = 5;
int operando2 = 7;
// Act: Ejecutar operación
int resultado = calc.sumar(operando1, operando2);
// Assert: Verificar resultado
assertEquals(12, resultado, "5 + 7 debe ser 12");
}
}Test con verificación de excepción¶
@Test
void testDividir_CuandoDivisorEsCero_LanzaArithmeticException() {
// Arrange: Preparar datos inválidos
Calculadora calc = new Calculadora();
int dividendo = 10;
int divisor = 0;
// Act & Assert: Verificar que se lance la excepción esperada
assertThrows(ArithmeticException.class, () -> {
calc.dividir(dividendo, divisor);
}, "Dividir por cero debe lanzar ArithmeticException");
}Test con setup complejo¶
@Test
void testProcesarPedido_ConMultiplesProductos_CalculaTotalCorrectamente() {
// Arrange: Setup complejo de múltiples objetos
Cliente cliente = new Cliente("María", "maria@ejemplo.com");
Producto prod1 = new Producto("Mouse", 25.0);
Producto prod2 = new Producto("Teclado", 45.0);
Producto prod3 = new Producto("Monitor", 150.0);
Pedido pedido = new Pedido(cliente);
pedido.agregarProducto(prod1, 2); // 2 mouses
pedido.agregarProducto(prod2, 1); // 1 teclado
pedido.agregarProducto(prod3, 1); // 1 monitor
GestorPedidos gestor = new GestorPedidos();
// Act: Procesar el pedido
ResumenPedido resumen = gestor.procesarPedido(pedido);
// Assert: Verificar cálculos
assertEquals(245.0, resumen.getTotal(), 0.01,
"Total: 2*25 + 1*45 + 1*150 = 245");
assertEquals(3, resumen.getCantidadProductos(),
"Debe haber 3 tipos de productos diferentes");
assertEquals(4, resumen.getCantidadItems(),
"Debe haber 4 items en total (2+1+1)");
}Separación visual con líneas en blanco¶
Para mejorar la legibilidad, separar las fases con líneas en blanco:
@Test
void testRegistrarUsuario_ConDatosValidos_GuardaEnBaseDeDatos() {
// Arrange
String nombre = "Carlos";
String email = "carlos@ejemplo.com";
RepositorioUsuarios repo = new RepositorioUsuarios();
// Act
Usuario usuario = repo.registrar(nombre, email);
// Assert
assertNotNull(usuario);
assertEquals(nombre, usuario.getNombre());
assertEquals(email, usuario.getEmail());
assertTrue(usuario.getId() > 0, "El ID debe ser asignado");
}Comentarios explícitos opcionales¶
Aunque no es obligatorio, es recomendable incluir comentarios que marquen cada fase:
@Test
void testBuscar_ConElementoExistente_RetornaIndice() {
// Arrange: Crear lista con elementos conocidos
List<String> lista = Arrays.asList("manzana", "banana", "pera");
Buscador buscador = new Buscador();
// Act: Buscar elemento que sabemos existe
int indice = buscador.buscar(lista, "banana");
// Assert: Debe retornar índice 1
assertEquals(1, indice);
}Casos especiales¶
Tests que verifican excepciones¶
En tests de excepciones, Act y Assert se combinan:
@Test
void testCrearUsuario_ConEmailInvalido_LanzaValidacionException() {
// Arrange
String nombre = "Ana";
String emailInvalido = "no-es-un-email";
RegistroUsuarios registro = new RegistroUsuarios();
// Act & Assert combinados
assertThrows(ValidacionException.class, () -> {
registro.crearUsuario(nombre, emailInvalido);
});
}Tests con verificación de estado¶
@Test
void testAgregarProducto_AlCarritoVacio_IncrementaCantidad() {
// Arrange
CarritoCompras carrito = new CarritoCompras();
Producto producto = new Producto("Libro", 20.0);
int cantidadInicial = carrito.getCantidadItems();
// Act
carrito.agregarProducto(producto);
// Assert: Verificar cambio de estado
assertEquals(cantidadInicial + 1, carrito.getCantidadItems());
assertTrue(carrito.contieneProducto(producto));
}Anti-patrones¶
Mezclar fases ❌¶
@Test
void testMalEstructurado() {
// ❌ Arrange y Act mezclados
Calculadora calc = new Calculadora();
int resultado = calc.sumar(2, 3); // Act prematuro
int otroNumero = 10; // Más Arrange después de Act
assertEquals(5, resultado); // Assert
assertEquals(15, calc.sumar(resultado, otroNumero)); // Otro Act+Assert
}Múltiples Acts ❌¶
@Test
void testMultiplesOperaciones() {
// Arrange
Calculadora calc = new Calculadora();
// ❌ Múltiples Acts - debería ser tests separados
int suma = calc.sumar(2, 3);
int resta = calc.restar(5, 2);
int mult = calc.multiplicar(3, 4);
// Assert mezclado
assertEquals(5, suma);
assertEquals(3, resta);
assertEquals(12, mult);
}0x4002 - Una llamada a método en cada caso de prueba¶
Explicación¶
Cada test debe verificar una única funcionalidad realizando una sola llamada al método bajo prueba. Un test debe tener un único propósito y verificar una sola cosa. Tests con múltiples llamadas son más difíciles de mantener, depurar y entender cuando fallan.
Justificación¶
Foco único: Cada test verifica un comportamiento específico.
Diagnóstico preciso: Cuando falla, es obvio qué funcionalidad está rota.
Mantenibilidad: Modificar un comportamiento requiere tocar un solo test.
Principio SRP: Single Responsibility Principle aplicado a tests.
Reportes claros: Los resultados de tests indican exactamente qué funciona y qué no.
Sintaxis conceptual¶
TEST = ARRANGE + UNA_LLAMADA + ASSERT
Donde UNA_LLAMADA es la ejecución del método bajo pruebaEjemplos correctos¶
Test simple con una llamada¶
public class CalculadoraTest {
@Test
void testSumar_ConDosNumerosPositivos_RetornaSuma() {
// Arrange
Calculadora calc = new Calculadora();
// Act: UNA llamada
int resultado = calc.sumar(5, 3);
// Assert
assertEquals(8, resultado);
}
@Test
void testRestar_ConMinuendo MayorQueSustraendo_RetornaDiferenciaPositiva() {
// Arrange
Calculadora calc = new Calculadora();
// Act: UNA llamada
int resultado = calc.restar(10, 4);
// Assert
assertEquals(6, resultado);
}
}Verificación de múltiples propiedades del resultado¶
Es válido tener múltiples assertions sobre el mismo resultado:
@Test
void testCrearUsuario_ConDatosValidos_RetornaUsuarioCompleto() {
// Arrange
String nombre = "María";
String email = "maria@ejemplo.com";
RegistroUsuarios registro = new RegistroUsuarios();
// Act: UNA llamada
Usuario usuario = registro.crearUsuario(nombre, email);
// Assert: Múltiples verificaciones sobre el MISMO objeto retornado
assertNotNull(usuario, "No debe retornar null");
assertEquals(nombre, usuario.getNombre(), "El nombre debe coincidir");
assertEquals(email, usuario.getEmail(), "El email debe coincidir");
assertTrue(usuario.getId() > 0, "Debe asignar un ID positivo");
assertNotNull(usuario.getFechaCreacion(), "Debe asignar fecha de creación");
}Verificación de estado del objeto¶
También es válido verificar el estado resultante después de una operación:
@Test
void testAgregarProducto_AlCarritoVacio_ActualizaEstadoCarrito() {
// Arrange
CarritoCompras carrito = new CarritoCompras();
Producto producto = new Producto("Mouse", 25.0);
// Act: UNA llamada al método bajo prueba
carrito.agregarProducto(producto);
// Assert: Verificar cambios en el estado del objeto
assertEquals(1, carrito.getCantidadItems());
assertTrue(carrito.contieneProducto(producto));
assertEquals(25.0, carrito.getTotal(), 0.01);
}Ejemplos incorrectos¶
Múltiples llamadas al código bajo prueba ❌¶
@Test
void testOperacionesMatematicas() { // ❌ Nombre genérico
// Arrange
Calculadora calc = new Calculadora();
// Act: ❌ MÚLTIPLES llamadas - viola la regla
int suma = calc.sumar(2, 3);
int resta = calc.restar(5, 2);
int multiplicacion = calc.multiplicar(3, 4);
int division = calc.dividir(10, 2);
// Assert
assertEquals(5, suma);
assertEquals(3, resta);
assertEquals(12, multiplicacion);
assertEquals(5, division);
}Problema: Si alguna assertion falla, no sabés cuál de las cuatro operaciones tiene el bug. Además, si falla la primera assertion, las demás no se ejecutan.
Solución correcta: Tests separados ✅¶
@Test
void testSumar_ConDosNumerosPositivos_RetornaSuma() {
// Arrange
Calculadora calc = new Calculadora();
// Act
int resultado = calc.sumar(2, 3);
// Assert
assertEquals(5, resultado);
}
@Test
void testRestar_ConMinuendoMayorQueSustraendo_RetornaDiferencia() {
// Arrange
Calculadora calc = new Calculadora();
// Act
int resultado = calc.restar(5, 2);
// Assert
assertEquals(3, resultado);
}
@Test
void testMultiplicar_ConDosFactores_RetornaProducto() {
// Arrange
Calculadora calc = new Calculadora();
// Act
int resultado = calc.multiplicar(3, 4);
// Assert
assertEquals(12, resultado);
}
@Test
void testDividir_ConDivisorNoNulo_RetornaCociente() {
// Arrange
Calculadora calc = new Calculadora();
// Act
int resultado = calc.dividir(10, 2);
// Assert
assertEquals(5, resultado);
}Casos especiales permitidos¶
Setup compartido con @BeforeEach¶
Cuando múltiples tests requieren el mismo setup, usá @BeforeEach:
public class GestorCuentasTest {
private GestorCuentas gestor;
private Cliente clientePrueba;
@BeforeEach
void setUp() {
// Setup común para todos los tests
gestor = new GestorCuentas();
clientePrueba = new Cliente("Juan", "juan@ejemplo.com");
}
@Test
void testCrearCuenta_ConClienteNuevo_RetornaCuentaActiva() {
// Arrange: Ya tenemos gestor y cliente desde setUp()
double saldoInicial = 100.0;
// Act: UNA llamada
Cuenta cuenta = gestor.crearCuenta(clientePrueba, saldoInicial);
// Assert
assertEquals(EstadoCuenta.ACTIVA, cuenta.getEstado());
assertEquals(saldoInicial, cuenta.getSaldo(), 0.01);
}
@Test
void testCerrarCuenta_ConSaldoCero_CambiEstadoACerrada() {
// Arrange
Cuenta cuenta = gestor.crearCuenta(clientePrueba, 0.0);
// Act: UNA llamada
gestor.cerrarCuenta(cuenta);
// Assert
assertEquals(EstadoCuenta.CERRADA, cuenta.getEstado());
}
}Llamadas auxiliares en Arrange¶
Podés hacer llamadas auxiliares durante la preparación, siempre que no sean el foco del test:
@Test
void testTransferir_EntreDosCuentas_ActualizaSaldos() {
// Arrange: Llamadas auxiliares para preparar el contexto
GestorCuentas gestor = new GestorCuentas();
Cuenta origen = gestor.crearCuenta("Cuenta Origen", 500.0); // Auxiliar
Cuenta destino = gestor.crearCuenta("Cuenta Destino", 100.0); // Auxiliar
double montoTransferencia = 200.0;
// Act: UNA llamada al método bajo prueba
gestor.transferir(origen, destino, montoTransferencia);
// Assert: Verificar resultado de la transferencia
assertEquals(300.0, origen.getSaldo(), 0.01);
assertEquals(300.0, destino.getSaldo(), 0.01);
}Tests parametrizados como excepción¶
JUnit 5 permite tests parametrizados que prueban el mismo comportamiento con diferentes datos:
@ParameterizedTest
@CsvSource({
"2, 3, 5",
"10, 20, 30",
"-5, 5, 0",
"0, 0, 0"
})
void testSumar_ConDiferentesValores_RetornaResultadoCorrecto(
int a, int b, int esperado) {
// Arrange
Calculadora calc = new Calculadora();
// Act: UNA llamada por cada ejecución parametrizada
int resultado = calc.sumar(a, b);
// Assert
assertEquals(esperado, resultado);
}Beneficios de seguir esta regla¶
// ❌ Test malo: Prueba múltiples cosas
@Test
void testOperaciones() {
Calculadora calc = new Calculadora();
assertEquals(5, calc.sumar(2, 3));
assertEquals(3, calc.restar(5, 2));
assertEquals(12, calc.multiplicar(3, 4));
}
// Si falla la suma, nunca sabemos si resta y multiplicación funcionan
// ✅ Tests buenos: Uno por funcionalidad
@Test
void testSumar_ConDosNumeros_RetornaSuma() {
Calculadora calc = new Calculadora();
assertEquals(5, calc.sumar(2, 3));
}
@Test
void testRestar_ConDosNumeros_RetornaResta() {
Calculadora calc = new Calculadora();
assertEquals(3, calc.restar(5, 2));
}
@Test
void testMultiplicar_ConDosNumeros_RetornaProducto() {
Calculadora calc = new Calculadora();
assertEquals(12, calc.multiplicar(3, 4));
}
// Cada test falla independientemente, diagnóstico preciso0x4003 - Los nombres de tests deben ser descriptivos y usar convención test<Accion><Condicion><ResultadoEsperado>¶
Explicación¶
Los nombres de los métodos de test deben ser altamente descriptivos y seguir una convención que haga obvio: qué se está probando, bajo qué condiciones y qué resultado se espera. A diferencia del código de producción donde se prefieren nombres cortos, en tests se privilegia la claridad absoluta sobre la brevedad.
Justificación¶
Documentación viva: Los nombres de tests documentan el comportamiento esperado del sistema.
Reportes claros: Cuando un test falla, el nombre indica exactamente qué funcionalidad está rota.
Especificación ejecutable: Los nombres actúan como especificación del comportamiento del código.
Mantenimiento: Facilita entender qué hace cada test sin leer el código completo.
Diseño guiado por tests: Nombres descriptivos ayudan a pensar en casos límite y condiciones.
Convención recomendada¶
test<MetodoAProbar>_<CondicionOContexto>_<ResultadoEsperado>
Componentes:
- test: Prefijo estándar (obligatorio en JUnit 3/4, opcional en JUnit 5)
- MetodoAProbar: Nombre del método que se está probando
- CondicionOContexto: Bajo qué circunstancias
- ResultadoEsperado: Qué debe sucederEjemplos correctos¶
Tests de métodos que retornan valores¶
public class CalculadoraTest {
@Test
void testSumar_ConDosNumerosPositivos_RetornaSuma() {
Calculadora calc = new Calculadora();
assertEquals(8, calc.sumar(5, 3));
}
@Test
void testSumar_ConUnNumeroNegativo_RetornaResultadoCorrecto() {
Calculadora calc = new Calculadora();
assertEquals(2, calc.sumar(5, -3));
}
@Test
void testDividir_ConDosNumerosValidos_RetornaCociente() {
Calculadora calc = new Calculadora();
assertEquals(5.0, calc.dividir(10.0, 2.0), 0.001);
}
@Test
void testDividir_ConDivisorCero_LanzaArithmeticException() {
Calculadora calc = new Calculadora();
assertThrows(ArithmeticException.class, () -> calc.dividir(10, 0));
}
}Tests de métodos que modifican estado¶
public class ListaTest {
@Test
void testAgregar_EnListaVacia_IncrementaTamanioAUno() {
Lista<String> lista = new Lista<>();
lista.agregar("elemento");
assertEquals(1, lista.tamanio());
}
@Test
void testRemover_ElementoExistente_DecrementaTamanio() {
Lista<String> lista = new Lista<>();
lista.agregar("elemento");
lista.remover("elemento");
assertEquals(0, lista.tamanio());
}
@Test
void testRemover_ElementoNoExistente_NoModificaLista() {
Lista<String> lista = new Lista<>();
lista.agregar("elemento1");
lista.remover("elemento2");
assertEquals(1, lista.tamanio());
}
}Tests de métodos booleanos¶
public class ValidadorEmailTest {
@Test
void testEsValido_ConEmailCorrecto_RetornaTrue() {
ValidadorEmail validador = new ValidadorEmail();
assertTrue(validador.esValido("usuario@ejemplo.com"));
}
@Test
void testEsValido_SinArroba_RetornaFalse() {
ValidadorEmail validador = new ValidadorEmail();
assertFalse(validador.esValido("usuarioejemplo.com"));
}
@Test
void testEsValido_SinDominio_RetornaFalse() {
ValidadorEmail validador = new ValidadorEmail();
assertFalse(validador.esValido("usuario@"));
}
@Test
void testEsValido_ConEspacios_RetornaFalse() {
ValidadorEmail validador = new ValidadorEmail();
assertFalse(validador.esValido("usuario @ejemplo.com"));
}
}Ejemplos incorrectos¶
Nombres demasiado genéricos ❌¶
// ❌ No indica qué se está probando
@Test
void test1() {
// ...
}
// ❌ No indica condición ni resultado esperado
@Test
void testSumar() {
// ...
}
// ❌ Nombre ambiguo
@Test
void testCalculadora() {
// ¿Qué método? ¿Qué condición?
}
// ❌ No indica resultado esperado
@Test
void testDividirPorCero() {
// ¿Debe fallar? ¿Retornar cero? ¿Retornar infinito?
}Nombres sin estructura ❌¶
// ❌ Sin convención clara
@Test
void sumaTest() {
// ...
}
// ❌ Orden incorrecto
@Test
void cuandoSeAgregaUnElementoLaListaCreceDeTamanio() {
// Demasiado narrativo, difícil de escanear visualmente
}
// ❌ Abreviaciones innecesarias
@Test
void testCalcDescCli() {
// ¿Qué significa? No es obvio
}Convenciones alternativas válidas¶
Además de la convención principal, estas alternativas son aceptables:
Notación con guiones bajos para legibilidad¶
@Test
void test_sumar_con_dos_numeros_positivos_retorna_suma() {
// Algunos equipos prefieren guiones bajos para mejorar legibilidad
Calculadora calc = new Calculadora();
assertEquals(8, calc.sumar(5, 3));
}Notación narrativa (Given-When-Then)¶
@Test
void givenDosNumerosPositivos_whenSumar_thenRetornaSuma() {
// Inspirado en BDD (Behavior-Driven Development)
Calculadora calc = new Calculadora();
assertEquals(8, calc.sumar(5, 3));
}Notación solo descriptiva (sin prefijo test)¶
// JUnit 5 permite omitir el prefijo "test"
@Test
void sumarDosNumerosPositivosRetornaSuma() {
Calculadora calc = new Calculadora();
assertEquals(8, calc.sumar(5, 3));
}Estructura recomendada por componente¶
Acción (Qué se prueba)¶
Usar el nombre del método exacto:
testSumar_... // Para método sumar()
testDividir_... // Para método dividir()
testCalcularDescuento_... // Para método calcularDescuento()
testCrearUsuario_... // Para método crearUsuario()Condición (Bajo qué circunstancias)¶
Describir el contexto o precondición:
testDividir_ConDivisorCero_...
testBuscar_EnListaVacia_...
testCalcularDescuento_ConClientePremium_...
testRegistrarUsuario_ConEmailDuplicado_...
testProcesar_ConConexionCerrada_...Resultado esperado (Qué debe suceder)¶
Describir claramente el comportamiento esperado:
..._RetornaCociente
..._LanzaIllegalArgumentException
..._RetornaListaVacia
..._GuardaEnBaseDeDatos
..._NoModificaElEstado
..._RetornaNull
..._AplicaDescuentoDel20PorcientoEjemplos del mundo real¶
Test de repositorio¶
public class RepositorioUsuariosTest {
@Test
void testBuscarPorId_ConIdExistente_RetornaUsuario() {
// ...
}
@Test
void testBuscarPorId_ConIdNoExistente_RetornaNull() {
// ...
}
@Test
void testGuardar_ConUsuarioNuevo_AsignaId() {
// ...
}
@Test
void testGuardar_ConUsuarioExistente_ActualizaDatos() {
// ...
}
@Test
void testEliminar_ConIdExistente_RetornaTrue() {
// ...
}
@Test
void testEliminar_ConIdNoExistente_RetornaFalse() {
// ...
}
}Test de validador¶
public class ValidadorContrasenaTest {
@Test
void testEsSegura_ConLongitudMinima8Caracteres_RetornaTrue() {
// ...
}
@Test
void testEsSegura_ConMenosDe8Caracteres_RetornaFalse() {
// ...
}
@Test
void testEsSegura_SinMayusculas_RetornaFalse() {
// ...
}
@Test
void testEsSegura_SinNumeros_RetornaFalse() {
// ...
}
@Test
void testEsSegura_ConTodosLosRequisitos_RetornaTrue() {
// ...
}
@Test
void testValidar_ConContrasenaNull_LanzaIllegalArgumentException() {
// ...
}
}Test de clase con lógica de negocio¶
public class CarritoComprasTest {
@Test
void testCalcularTotal_ConCarritoVacio_RetornaCero() {
// ...
}
@Test
void testCalcularTotal_ConUnProducto_RetornaPrecioProducto() {
// ...
}
@Test
void testCalcularTotal_ConMultiplesProductos_RetornaSumaPrecios() {
// ...
}
@Test
void testAplicarDescuento_ConCodigoValido_ReduceTotal() {
// ...
}
@Test
void testAplicarDescuento_ConCodigoInvalido_LanzaDescuentoInvalidoException() {
// ...
}
@Test
void testAplicarDescuento_ConCodigoYaUsado_LanzaDescuentoException() {
// ...
}
}Patrones de nombres para casos comunes¶
Valores límite¶
testCalcular_ConValorCero_...
testCalcular_ConValorMaximo_...
testCalcular_ConValorMinimo_...
testCalcular_ConValorNegativo_...Colecciones¶
testProcesar_ConListaVacia_...
testProcesar_ConUnSoloElemento_...
testProcesar_ConMultiplesElementos_...
testProcesar_ConListaNull_...Estados de objeto¶
testOperacion_ConObjetoNuevo_...
testOperacion_ConObjetoInicializado_...
testOperacion_ConObjetoModificado_...
testOperacion_ConObjetoNull_...Operaciones de modificación¶
testAgregar_..._IncrementaTamanio
testRemover_..._DecrementaTamanio
testModificar_..._ActualizaValor
testLimpiar_..._DejaObjetoEnEstadoInicialLongitud de nombres¶
Aunque los nombres de tests son naturalmente más largos que los del código de producción, intentá mantener un equilibrio:
// ✅ Descriptivo pero razonable
@Test
void testDividir_ConDivisorCero_LanzaArithmeticException() {
// ...
}
// ⚠️ Excesivamente largo - considerar simplificar
@Test
void testCalcularElDescuentoAplicableParaClientesPremiumDuranteElMesDeSeptiembre() {
// Demasiado específico, dificulta refactoring
}
// ✅ Mejor versión simplificada
@Test
void testCalcularDescuento_ClientePremiumEnSeptiembre_AplicaDescuentoEspecial() {
// Más conciso pero sigue siendo claro
}Agrupación de tests relacionados¶
Para tests relacionados, usar nombres que se agrupen naturalmente:
public class ValidadorTest {
// Grupo: tests de validación de formato
@Test
void testValidarFormato_ConFormatoCorrecto_RetornaTrue() { }
@Test
void testValidarFormato_ConFormatoIncorrecto_RetornaFalse() { }
// Grupo: tests de validación de longitud
@Test
void testValidarLongitud_ConLongitudMinima_RetornaTrue() { }
@Test
void testValidarLongitud_MenorAMinima_RetornaFalse() { }
@Test
void testValidarLongitud_MayorAMaxima_RetornaFalse() { }
// Grupo: tests de validación completa
@Test
void testValidar_ConTodosLosCriteriosCumplidos_RetornaTrue() { }
@Test
void testValidar_ConAlgunCriterioIncumplido_RetornaFalse() { }
}Anotación @DisplayName como alternativa¶
JUnit 5 ofrece @DisplayName para nombres más legibles en reportes:
public class CalculadoraTest {
@Test
@DisplayName("Sumar dos números positivos retorna la suma correcta")
void testSumarDosNumerosPositivos() {
Calculadora calc = new Calculadora();
assertEquals(8, calc.sumar(5, 3));
}
@Test
@DisplayName("Dividir por cero lanza ArithmeticException")
void testDividirPorCero() {
Calculadora calc = new Calculadora();
assertThrows(ArithmeticException.class, () -> calc.dividir(10, 0));
}
}Tests parametrizados¶
Los tests parametrizados pueden tener nombres más genéricos porque la parametrización añade contexto:
@ParameterizedTest
@DisplayName("Sumar diferentes pares de números retorna el resultado correcto")
@CsvSource({
"2, 3, 5",
"10, 20, 30",
"-5, 5, 0",
"0, 0, 0",
"-10, -5, -15"
})
void testSumar_ConDiferentesValores_RetornaResultadoCorrecto(
int a, int b, int esperado) {
Calculadora calc = new Calculadora();
assertEquals(esperado, calc.sumar(a, b));
}Anti-patrones comunes¶
Nombres sin información ❌¶
// ❌ Totalmente inútil
@Test
void test1() { }
@Test
void test2() { }
@Test
void testA() { }
@Test
void testMetodo() { }Nombres ambiguos ❌¶
// ❌ ¿Qué debería hacer?
@Test
void testCalcular() { }
// ❌ ¿Qué se verifica?
@Test
void testUsuario() { }
// ❌ ¿Cuál es el caso específico?
@Test
void testValidacion() { }Nombres que describen implementación en lugar de comportamiento ❌¶
// ❌ Describe cómo funciona internamente, no qué hace
@Test
void testIteraSobreArregloYSumaElementos() { }
// ✅ Describe qué hace desde perspectiva del usuario
@Test
void testCalcularSuma_ConArregloDeNumeros_RetornaSumaTotal() { }Guía práctica¶
Para nombrar un test, hacete estas preguntas:
¿Qué método estoy probando? → Parte de “Acción”
¿Bajo qué condiciones? → Parte de “Condición”
¿Qué debería pasar? → Parte de “Resultado”
Ejemplo práctico:
1. Método: buscarPorEmail()
2. Condición: Con un email que existe en la base
3. Resultado: Retorna el usuario correspondiente
→ testBuscarPorEmail_ConEmailExistente_RetornaUsuario1. Método: procesarPago()
2. Condición: Con saldo insuficiente
3. Resultado: Lanza SaldoInsuficienteException
→ testProcesarPago_ConSaldoInsuficiente_LanzaSaldoInsuficienteExceptionLectura del reporte de tests¶
Buenos nombres producen reportes auto-explicativos:
CalculadoraTest
✅ testSumar_ConDosNumerosPositivos_RetornaSuma
✅ testSumar_ConUnNumeroNegativo_RetornaResultadoCorrecto
✅ testDividir_ConDosNumerosValidos_RetornaCociente
❌ testDividir_ConDivisorCero_LanzaArithmeticException
✅ testMultiplicar_ConFactorCero_RetornaCeroAl ver el reporte, inmediatamente sabés que la división por cero no está funcionando correctamente.
0x4004 - Los tests no deben tener lógica condicional¶
Explicación¶
Los tests no deben contener estructuras de control de flujo como if, else, switch, for, while o do-while. Si un test necesita lógica condicional, probablemente está intentando probar múltiples escenarios en un solo test y debe dividirse en tests separados.
Justificación¶
Simplicidad: Los tests deben ser simples y directos, sin caminos alternativos.
Determinismo: Sin condicionales, el test siempre ejecuta el mismo código.
Cobertura clara: Cada test cubre un camino específico, sin ambigüedad.
Fallos precisos: Cuando falla, sabés exactamente qué escenario está roto.
Tests no testeables: Si el test tiene lógica, ¿quién prueba el test?
Anti-patrón: Condicional en test ❌¶
// ❌ INCORRECTO: Lógica condicional en el test
@Test
void testCalcular() {
Calculadora calc = new Calculadora();
int numero = 5;
int resultado = calc.calcular(numero);
if (numero % 2 == 0) {
assertEquals(numero * 2, resultado);
} else {
assertEquals(numero * 3, resultado);
}
}Solución correcta: Tests separados ✅¶
// ✅ CORRECTO: Test separado para cada caso
@Test
void testCalcular_ConNumeroPar_RetornaDoble() {
Calculadora calc = new Calculadora();
int numeroPar = 4;
int resultado = calc.calcular(numeroPar);
assertEquals(8, resultado);
}
@Test
void testCalcular_ConNumeroImpar_RetornaTriple() {
Calculadora calc = new Calculadora();
int numeroImpar = 5;
int resultado = calc.calcular(numeroImpar);
assertEquals(15, resultado);
}Anti-patrón: Bucle en test ❌¶
// ❌ INCORRECTO: Bucle en el test
@Test
void testSumar() {
Calculadora calc = new Calculadora();
for (int i = 0; i < 10; i++) {
int resultado = calc.sumar(i, i);
assertEquals(i * 2, resultado);
}
}Solución: Test parametrizado ✅¶
// ✅ CORRECTO: Test parametrizado
@ParameterizedTest
@CsvSource({
"0, 0, 0",
"1, 1, 2",
"5, 5, 10",
"9, 9, 18"
})
void testSumar_ConNumerosSumadosASiMismos_RetornaElDoble(
int numero, int mismo, int esperado) {
Calculadora calc = new Calculadora();
assertEquals(esperado, calc.sumar(numero, mismo));
}0x4005 - Cada test debe ser independiente y poder ejecutarse en cualquier orden¶
Explicación¶
Los tests deben ser completamente independientes entre sí: cada test debe preparar su propio estado, ejecutar su verificación y limpiar recursos sin depender de que otros tests se hayan ejecutado antes o después. El orden de ejecución de los tests no debe afectar los resultados.
Justificación¶
Ejecución paralela: Los frameworks modernos ejecutan tests en paralelo para velocidad.
Depuración: Podés ejecutar un solo test sin preocuparte por setup previo.
Mantenimiento: Agregar o eliminar tests no afecta a los demás.
Determinismo: Los resultados son consistentes independientemente del orden.
Aislamiento de fallos: Un test que falla no hace fallar a otros.
Principio de independencia¶
CADA TEST = SETUP + EJECUCIÓN + VERIFICACIÓN + CLEANUP
No debe existir:
- Estado compartido mutable entre tests
- Dependencias de orden de ejecución
- Efectos colaterales que afecten otros testsEjemplos correctos¶
Independencia con BeforeEach¶
public class CuentaBancariaTest {
private CuentaBancaria cuenta;
@BeforeEach
void setUp() {
// Cada test recibe una cuenta nueva e independiente
cuenta = new CuentaBancaria("12345", 1000.0);
}
@Test
void testDepositar_ConMontoPositivo_IncrementaSaldo() {
cuenta.depositar(500.0);
assertEquals(1500.0, cuenta.getSaldo(), 0.01);
}
@Test
void testRetirar_ConMontoValido_DecrementaSaldo() {
cuenta.retirar(300.0);
assertEquals(700.0, cuenta.getSaldo(), 0.01);
}
@Test
void testRetirar_ConMontoMayorASaldo_LanzaSaldoInsuficienteException() {
assertThrows(SaldoInsuficienteException.class,
() -> cuenta.retirar(1500.0));
}
}Independencia sin estado compartido¶
public class CalculadoraTest {
@Test
void testSumar_ConDosNumeros_RetornaSuma() {
// Cada test crea su propia instancia
Calculadora calc = new Calculadora();
assertEquals(5, calc.sumar(2, 3));
}
@Test
void testRestar_ConDosNumeros_RetornaResta() {
// Independiente del test anterior
Calculadora calc = new Calculadora();
assertEquals(2, calc.restar(5, 3));
}
}Anti-patrón: Estado compartido mutable ❌¶
Problema: Tests que comparten estado¶
// ❌ INCORRECTO: Estado compartido mutable
public class ContadorTest {
private static int contador = 0; // ❌ Estado compartido
@Test
void testIncrementar() {
contador++; // ❌ Modifica estado compartido
assertEquals(1, contador); // ❌ Depende del orden
}
@Test
void testIncrementarDosVeces() {
contador += 2; // ❌ Modifica estado compartido
assertEquals(3, contador); // ❌ Asume que testIncrementar() se ejecutó primero
}
}Problema: Si testIncrementarDosVeces se ejecuta primero, contador sería 2, no 3. Si se ejecutan en paralelo, hay condiciones de carrera.
Solución correcta ✅¶
// ✅ CORRECTO: Cada test es independiente
public class ContadorTest {
@Test
void testIncrementar_DesdeEstadoInicial_ResultaEnUno() {
Contador contador = new Contador(); // ✅ Instancia propia
contador.incrementar();
assertEquals(1, contador.getValor());
}
@Test
void testIncrementar_DosVecesDesdeEstadoInicial_ResultaEnDos() {
Contador contador = new Contador(); // ✅ Instancia propia
contador.incrementar();
contador.incrementar();
assertEquals(2, contador.getValor());
}
}Anti-patrón: Dependencia de orden ❌¶
Problema: Tests que dependen de ejecución previa¶
// ❌ INCORRECTO: Tests con dependencia de orden
public class BaseDeDatosTest {
private static BaseDeDatos bd;
@Test
void test1_Conectar() {
bd = new BaseDeDatos(); // ❌ Inicializa variable estática
bd.conectar();
assertTrue(bd.estaConectada());
}
@Test
void test2_GuardarDatos() {
// ❌ Asume que test1_Conectar() ya se ejecutó
bd.guardar("dato");
assertEquals(1, bd.contarRegistros());
}
@Test
void test3_ConsultarDatos() {
// ❌ Asume que test2_GuardarDatos() ya se ejecutó
String dato = bd.consultar(0);
assertEquals("dato", dato);
}
}Problemas:
Si test2 se ejecuta antes que test1, falla con NullPointerException
No podés ejecutar test3 aisladamente
Ejecución paralela causaría fallos aleatorios
Solución correcta ✅¶
// ✅ CORRECTO: Cada test es autosuficiente
public class BaseDeDatosTest {
private BaseDeDatos bd;
@BeforeEach
void setUp() {
// Cada test recibe una BD nueva y conectada
bd = new BaseDeDatos();
bd.conectar();
}
@AfterEach
void tearDown() {
// Limpieza después de cada test
if (bd != null && bd.estaConectada()) {
bd.desconectar();
}
}
@Test
void testGuardar_ConDatoValido_GuardaEnBaseDeDatos() {
// ✅ Independiente - prepara todo lo que necesita
bd.guardar("dato");
assertEquals(1, bd.contarRegistros());
}
@Test
void testConsultar_ConDatoGuardado_RetornaDato() {
// ✅ Independiente - prepara su propio contexto
bd.guardar("dato_especifico");
String resultado = bd.consultar(0);
assertEquals("dato_especifico", resultado);
}
@Test
void testConectar_ConCredencialesValidas_EstableceConexion() {
// ✅ Independiente - verifica conexión solamente
assertTrue(bd.estaConectada());
}
}Anti-patrón: Efectos colaterales en recursos compartidos ❌¶
Problema: Modificar archivos o bases de datos compartidas¶
// ❌ INCORRECTO: Efectos colaterales en archivo compartido
public class GestorArchivosTest {
private static final String ARCHIVO = "datos.txt";
@Test
void testGuardar_ConDato_EscribeEnArchivo() {
GestorArchivos gestor = new GestorArchivos();
gestor.guardar(ARCHIVO, "dato1"); // ❌ Modifica archivo compartido
String contenido = gestor.leer(ARCHIVO);
assertEquals("dato1", contenido);
}
@Test
void testAgregar_ConDato_AnadeAlFinal() {
GestorArchivos gestor = new GestorArchivos();
// ❌ Asume que el archivo ya existe con "dato1"
gestor.agregar(ARCHIVO, "dato2");
String contenido = gestor.leer(ARCHIVO);
assertTrue(contenido.contains("dato2"));
}
}Solución correcta: Archivos temporales únicos ✅¶
// ✅ CORRECTO: Cada test usa su propio archivo temporal
public class GestorArchivosTest {
private GestorArchivos gestor;
private String archivoTemporal;
@BeforeEach
void setUp() throws IOException {
gestor = new GestorArchivos();
// Crear archivo temporal único para este test
File temp = File.createTempFile("test_", ".txt");
archivoTemporal = temp.getAbsolutePath();
temp.deleteOnExit();
}
@Test
void testGuardar_ConDato_EscribeEnArchivo() throws IOException {
// ✅ Usa archivo propio
gestor.guardar(archivoTemporal, "dato1");
String contenido = gestor.leer(archivoTemporal);
assertEquals("dato1", contenido);
}
@Test
void testAgregar_EnArchivoNuevo_CreaDato() throws IOException {
// ✅ Independiente - prepara su archivo
gestor.guardar(archivoTemporal, "dato_inicial");
gestor.agregar(archivoTemporal, "dato2");
String contenido = gestor.leer(archivoTemporal);
assertTrue(contenido.contains("dato2"));
}
}Verificar independencia¶
Test de orden aleatorio¶
JUnit permite ejecutar tests en orden aleatorio:
@TestMethodOrder(MethodOrderer.Random.class)
public class MiTest {
// Tests se ejecutarán en orden aleatorio
}Ejecución paralela¶
// En junit-platform.properties
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrentCasos especiales permitidos¶
Estado compartido inmutable está bien¶
public class MatemáticasTest {
// ✅ Constante compartida es aceptable (inmutable)
private static final double DELTA = 0.0001;
@Test
void testCalcularCircunferencia_ConRadio1_RetornaDosPi() {
double resultado = Matematicas.calcularCircunferencia(1.0);
assertEquals(2 * Math.PI, resultado, DELTA);
}
@Test
void testCalcularArea_ConRadio1_RetornaPi() {
double resultado = Matematicas.calcularArea(1.0);
assertEquals(Math.PI, resultado, DELTA);
}
}Setup pesado con BeforeAll¶
Para recursos muy costosos de crear, @BeforeAll es aceptable si el recurso es de solo lectura:
public class AnalizadorTextoTest {
private static BaseDeDatosDiccionario diccionario;
@BeforeAll
static void setupClass() {
// ✅ Setup una sola vez (pero solo si es read-only)
diccionario = new BaseDeDatosDiccionario();
diccionario.cargarDesdeArchivo("diccionario.dat");
}
@Test
void testBuscarPalabra_ConPalabraExistente_RetornaDefinicion() {
// ✅ Solo lee del diccionario, no lo modifica
String definicion = diccionario.buscar("computadora");
assertNotNull(definicion);
}
@Test
void testBuscarPalabra_ConPalabraInexistente_RetornaNull() {
// ✅ Solo lee del diccionario, no lo modifica
String definicion = diccionario.buscar("xyzabc123");
assertNull(definicion);
}
}Comparación: Test dependiente vs independiente¶
Tests dependientes ❌¶
// ❌ INCORRECTO: Tests con dependencias
public class PilaTest {
private static Pila<String> pila = new Pila<>(); // ❌ Estado compartido
@Test
void test1_Apilar() {
pila.apilar("elemento1");
assertEquals(1, pila.tamanio());
}
@Test
void test2_ApilarDosVeces() {
// ❌ Asume que test1 ya ejecutó
pila.apilar("elemento2");
assertEquals(2, pila.tamanio()); // Falla si test1 no se ejecutó
}
@Test
void test3_Desapilar() {
// ❌ Asume que test1 y test2 ya ejecutaron
String elemento = pila.desapilar();
assertEquals("elemento2", elemento); // Depende de orden
}
}Tests independientes ✅¶
// ✅ CORRECTO: Tests independientes
public class PilaTest {
private Pila<String> pila;
@BeforeEach
void setUp() {
// Cada test recibe una pila nueva
pila = new Pila<>();
}
@Test
void testApilar_EnPilaVacia_IncrementaTamanioAUno() {
pila.apilar("elemento1");
assertEquals(1, pila.tamanio());
}
@Test
void testApilar_DosElementos_IncrementaTamanioADos() {
// ✅ Prepara su propio contexto
pila.apilar("elemento1");
pila.apilar("elemento2");
assertEquals(2, pila.tamanio());
}
@Test
void testDesapilar_DePilaConDosElementos_RetornaUltimoApilado() {
// ✅ Totalmente independiente
pila.apilar("primero");
pila.apilar("segundo");
String elemento = pila.desapilar();
assertEquals("segundo", elemento);
}
}Recursos externos y bases de datos¶
Problema: Base de datos compartida¶
// ❌ INCORRECTO: Tests modifican BD compartida
public class UsuarioRepositorioTest {
private static UsuarioRepositorio repo = new UsuarioRepositorio();
@Test
void testGuardar() {
Usuario usuario = new Usuario("juan@ejemplo.com");
repo.guardar(usuario); // ❌ Modifica BD compartida
assertEquals(1, repo.contar());
}
@Test
void testBuscar() {
// ❌ Asume que testGuardar() ya insertó datos
Usuario encontrado = repo.buscarPorEmail("juan@ejemplo.com");
assertNotNull(encontrado);
}
}Solución: Base de datos en memoria o transacciones¶
// ✅ CORRECTO: Cada test con BD limpia
public class UsuarioRepositorioTest {
private UsuarioRepositorio repo;
private BaseDeDatos bd;
@BeforeEach
void setUp() {
// Crear BD en memoria única para cada test
bd = new BaseDeDatos("jdbc:h2:mem:test_" + UUID.randomUUID());
bd.inicializarEsquema();
repo = new UsuarioRepositorio(bd);
}
@AfterEach
void tearDown() {
// Limpiar después de cada test
bd.cerrar();
}
@Test
void testGuardar_ConUsuarioNuevo_LoGuardaEnBD() {
Usuario usuario = new Usuario("juan@ejemplo.com");
repo.guardar(usuario);
assertEquals(1, repo.contar());
}
@Test
void testBuscar_ConEmailExistente_RetornaUsuario() {
// ✅ Prepara sus propios datos
Usuario usuario = new Usuario("maria@ejemplo.com");
repo.guardar(usuario);
Usuario encontrado = repo.buscarPorEmail("maria@ejemplo.com");
assertNotNull(encontrado);
assertEquals("maria@ejemplo.com", encontrado.getEmail());
}
}Archivos temporales¶
Con JUnit 5 TempDir¶
public class ProcesadorArchivosTest {
@Test
void testProcesar_ConArchivoValido_GeneraSalida(@TempDir Path dirTemp) {
// ✅ Cada test recibe su propio directorio temporal
Path archivoEntrada = dirTemp.resolve("entrada.txt");
Path archivoSalida = dirTemp.resolve("salida.txt");
Files.writeString(archivoEntrada, "contenido");
ProcesadorArchivos proc = new ProcesadorArchivos();
proc.procesar(archivoEntrada, archivoSalida);
assertTrue(Files.exists(archivoSalida));
}
@Test
void testProcesar_ConArchivoVacio_GeneraArchivoVacio(@TempDir Path dirTemp) {
// ✅ Otro test con su propio directorio temporal
Path archivoEntrada = dirTemp.resolve("vacio.txt");
Path archivoSalida = dirTemp.resolve("salida.txt");
Files.writeString(archivoEntrada, "");
ProcesadorArchivos proc = new ProcesadorArchivos();
proc.procesar(archivoEntrada, archivoSalida);
assertEquals(0, Files.size(archivoSalida));
}
}Verificar independencia en la práctica¶
Ejecutar tests en orden inverso¶
# Si tus tests son independientes, esto debería funcionar
mvn test -Djunit.jupiter.execution.parallel.enabled=trueEjecutar un solo test¶
# Cada test debería poder ejecutarse solo
mvn test -Dtest=CalculadoraTest#testSumar_ConDosNumeros_RetornaSumaEjecutar subset de tests¶
# Cualquier combinación debería funcionar
mvn test -Dtest=CalculadoraTest#test*Sumar*Patrón: Builders para setup complejo¶
Cuando el setup es complejo, usar el patrón Builder mantiene la independencia:
public class PedidoTest {
@Test
void testProcesar_ConPedidoCompleto_CalculaTotalCorrectamente() {
// ✅ Builder crea objeto independiente
Pedido pedido = new PedidoBuilder()
.conCliente("María")
.conProducto("Laptop", 1000.0)
.conProducto("Mouse", 25.0)
.conDescuento(0.10)
.construir();
double total = pedido.calcularTotal();
assertEquals(922.5, total, 0.01);
}
@Test
void testProcesar_ConPedidoSinDescuento_CalculaSumaSimple() {
// ✅ Otro pedido independiente
Pedido pedido = new PedidoBuilder()
.conCliente("Juan")
.conProducto("Teclado", 50.0)
.construir();
double total = pedido.calcularTotal();
assertEquals(50.0, total, 0.01);
}
}Resumen de principios¶
// ❌ Tests dependientes - frágiles y no paralelizables
@Test void test1() { x = 1; }
@Test void test2() { x++; assertEquals(2, x); } // Depende de test1
// ✅ Tests independientes - robustos y paralelizables
@Test void test1() { int x = 1; assertEquals(1, x); }
@Test void test2() { int x = 1; x++; assertEquals(2, x); }