Características de buenos unit tests
Este artículo es una explicación sobre las características de buenos unit tests, incluyendo preguntas de examen, componentes clave y etiquetas.
Los buenos unit tests son el cinturón de seguridad de cualquier software. Te ayudan a detectar rápidamente si un cambio rompió algo y te dan confianza para refactorizar código sin miedo a errores de regresión. Tanto en exámenes como en el trabajo diario, se espera que no solo escribas tests, sino que también evalúes qué hace que un unit test sea bueno.
En pocas palabras
Los buenos unit tests son correctos, aislados, rápidos, expresivos, mantenibles y fáciles de ejecutar. Proporcionan retroalimentación rápida y confiable, localizan errores con precisión y permiten refactorizar de forma segura.
Descripción técnica resumida
Los unit tests comprueban la unidad más pequeña y comprobable de un programa, generalmente un único método o clase. Para cumplir su propósito, deben satisfacer varias características:
1. Correcto
Un test debe verificar exactamente el comportamiento que pretende documentar. Las suposiciones falsas o las expectativas imprecisas hacen que un test sea inútil. Un test correcto es simultáneamente una especificación del comportamiento esperado de la unidad.
2. Aislado
Un unit test no debería depender de sistemas externos como bases de datos, redes, sistemas de archivos o APIs. Reemplazas estas dependencias con test doubles como stubs, mocks, fakes o spies. Los tests aislados se ejecutan de manera estable y rápida.
3. Rápido
Un buen unit test se ejecuta en el rango de milisegundos. Esto permite ejecutar cientos o miles de tests varias veces al día sin interrumpir el flujo de desarrollo. Los tests lentos rápidamente se ignoran o se eliminan de la pipeline de CI.
4. Expresivo
El nombre del test, la estructura del test y el mensaje de error deben ser comprensibles de un vistazo. Una estructura AAA clara y assertions precisas ayudan a entender inmediatamente qué salió mal cuando falla.
5. Mantenible
Los tests deben estar estructurados tan limpiamente como el código de producción. Sin código duplicado, sin complejidad innecesaria, librerías auxiliares compartidas y dependencias claras hacen que los tests sean fáciles de mantener a largo plazo. Los buenos tests sobreviven a refactorizaciones sin cambios.
6. Fácil de ejecutar
Los tests deben ser ejecutables con un único comando. No deben requerir preparación manual, un entorno especial o dependencias temporales. Su resultado debe ser determinista: misma entrada, misma salida.
Puntos clave para el examen
- AAA (Arrange, Act, Assert): Cada unit test consta de tres partes: preparar los datos, ejecutar el método a probar y verificar el resultado.
- Test doubles: Un stub proporciona respuestas predefinidas, un mock verifica interacciones, un fake es una implementación simplificada, un spy registra llamadas.
- Nomenclatura: Un nombre de test como
methodName_condition_expectedResulthace el comportamiento esperado inmediatamente legible. - Casos límite y manejo de errores: No solo el happy path, sino también valores nulos, entradas vacías, valores límite y excepciones deben ser probados.
- Sin números “mágicos”: Las constantes y variables con nombres descriptivos hacen los tests más comprensibles y mantenibles.
- Assertions expresivas: Las assertions deben proporcionar un mensaje de error claro, por ejemplo
assertEquals(expected, actual, "Un cliente nuevo no debe recibir descuento"). - Independencia: Los tests no deben depender unos de otros. El orden de ejecución debería ser irrelevante.
- Determinismo: Un test debe producir el mismo resultado en cada ejecución. La aleatoriedad, la hora o estados externos no tienen lugar en unit tests.
- Un test, una responsabilidad: Cada test debe verificar exactamente un comportamiento. Se permiten múltiples assertions, pero deben estar relacionadas.
- Integración en CI/CD: Los unit tests deben ejecutarse automáticamente en la pipeline de build y detenerlo si fallan.
Componentes clave
-
Estructura del test (AAA) AAA significa Arrange, Act, Assert. En la fase Arrange preparas todos los objetos y datos de prueba necesarios. En la fase Act ejecutas el método que quieres probar. En la fase Assert verificas el resultado.
-
Test doubles (Stub, Mock, Fake, Spy) Los test doubles reemplazan dependencias reales. Un stub proporciona respuestas fijas, un mock verifica si ciertos métodos fueron llamados, un fake es una implementación simplificada y un spy registra llamadas.
-
Convenciones de nomenclatura Un buen nombre de test describe el escenario y el resultado esperado. Patrones como
methodName_condition_expectedResultconvierten los tests en documentación legible. -
Assertions Las assertions son las verificaciones al final de un test. Deben ser precisas y proporcionar un mensaje informativo cuando fallan.
-
Datos de prueba (Factories, Builder) Los datos de prueba deben ser fáciles de crear y repetibles. Las factories y builders ayudan a construir objetos complejos sin sobrecargar el test con boilerplate.
-
Aislamiento Un unit test verifica exactamente una unidad. Las bases de datos, redes, archivos u otros servicios se reemplazan con test doubles.
-
Velocidad Los unit tests deben ejecutarse en el rango de milisegundos. Solo así la suite de tests sigue siendo ejecutable con miles de tests y proporciona retroalimentación rápida.
-
Independencia Los tests no deben depender unos de otros. Cada test crea sus propios datos y no deja estados que afecten al siguiente test.
-
Determinismo Un test produce el mismo resultado en cada ejecución. La aleatoriedad, el tiempo, la red o estados globales se reemplazan con entradas controladas y test doubles.
-
Mantenibilidad Los tests son código. Se benefician de DRY, nombres claros, métodos pequeños y bajo acoplamiento. Los tests mantenibles sobreviven a refactorizaciones y permanecen comprensibles.
Ejemplo práctico (Cálculo de descuentos)
El siguiente ejemplo muestra un unit test típico en Java con JUnit. Se trata de un método berechneRabatt que calcula un descuento para un cliente y un artículo. El test verifica el caso en el que un cliente nuevo no recibe descuento para un artículo estándar.
¿Qué se muestra aquí?
- Nomenclatura clara: El nombre del test describe el método, la condición y el resultado esperado.
- Estructura AAA: Las secciones Arrange, Act y Assert están comentadas y claramente separadas.
- Enfoque: Solo se verifica un único comportamiento.
- Assertion expresiva: El mensaje de error explica por qué el test es importante.
// Naming: berechneRabatt_neukunde_standardartikel_erwartet0Prozent
@Test
public void berechneRabatt_neukunde_standardartikel_erwartet0Prozent() {
// Arrange
Kunde kunde = new Kunde(Kundentyp.NEUKUNDE);
Artikel artikel = new Artikel(Artikeltyp.STANDARD);
RabattService service = new RabattService();
// Act
int rabatt = service.berechneRabatt(kunde, artikel);
// Assert
assertEquals(0, rabatt, "Neukunde soll keinen Rabatt erhalten");
}
Explicación: El test está aislado porque no utiliza sistemas externos. Es rápido porque solo crea objetos simples. Es expresivo porque el nombre y el mensaje de error son inmediatamente comprensibles. Si el test falla, sabes de inmediato que el cálculo de descuentos para clientes nuevos es incorrecto.
Ventajas y desventajas
Ventajas
- Retroalimentación rápida: Los unit tests se ejecutan en milisegundos y proporcionan feedback inmediato sobre si un cambio funciona.
- Localización precisa de errores: Un test fallido señala directamente la unidad afectada, no un sistema completo.
- Refactoring seguro: Con una suite de tests sólida, puedes reestructurar código sin miedo a regresiones.
- Bajo costo de mantenimiento con buena estructura: Los tests limpios son fáciles de entender y adaptar cuando los requisitos cambian.
- Documentación viva: Los tests describen el comportamiento del software y ayudan a los nuevos miembros del equipo a incorporarse.
- Detección temprana de errores: Los defectos se encuentran antes de llegar a producción o a niveles superiores de testing.
Desventajas
- Inversión inicial: Escribir test doubles y datos de prueba de calidad requiere tiempo al principio.
- Riesgo de sobre-ingeniería: Demasiadas librerías auxiliares, métodos factory complejos o mocks anidados pueden hacer los tests más difíciles de mantener que el código de producción.
- Falsa seguridad: Un test que no verifica lo correcto puede mostrar resultados positivos mientras el código sigue siendo defectuoso.
- Mantenimiento de los propios tests: Con mala estructura, cada pequeño cambio requiere ajustar muchos tests, eliminando el beneficio.
- No reemplazan tests de integración: Los unit tests verifican piezas aisladas. No sustituyen pruebas que validen la interacción de múltiples componentes.
FAQ: Características de los buenos unit tests
1. ¿Qué es un unit test?
2. ¿Qué significa AAA en unit tests?
3. ¿Qué hace que un unit test sea correcto?
4. ¿Qué significa aislamiento en unit tests?
5. ¿Qué es un test double?
6. ¿Cuál es la diferencia entre un stub y un mock?
7. ¿Qué es un fake?
8. ¿Qué es un spy?
9. ¿Por qué los unit tests deben ser rápidos?
10. ¿Qué significa expresivo en relación a los tests?
11. ¿Cuál es una buena convención de nombres para unit tests?
nombreMetodo_condicion_resultadoEsperado, por ejemplo calcularDescuento_clienteNuevo_articuloEstandar_esperaCeroPorciento. Alternativamente funciona deberiaX_cuandoY.12. ¿Qué es una assertion?
assertEquals, assertTrue o assertThrows.13. ¿Qué son números mágicos en tests?
14. ¿Por qué los tests deben ser independientes entre sí?
15. ¿Qué significa determinista en unit tests?
16. ¿Qué se entiende por mantenibilidad de tests?
17. ¿Cuál es la diferencia entre unit tests, tests de integración y tests E2E?
18. ¿Qué son casos límite?
19. ¿Qué es un happy path?
20. ¿Qué es una test factory?
21. ¿Qué es seguridad de refactoring en tests?
22. ¿Qué es un test flaky?
23. ¿Qué es cobertura de código?
24. ¿Por qué un test debe tener una única responsabilidad?
25. ¿Qué significa fácil de ejecutar en unit tests?
Estrategia de aprendizaje
1. Iniciación: escribe tu primer unit test con AAA
Toma un método simple, por ejemplo uno que calcule el descuento para un cliente. Escribe un test que use la estructura AAA y nombre el test siguiendo el patrón nombreMetodo_condicion_resultadoEsperado.
Tarea: escribe un test para el método esAdulto(int edad), que debe devolver true si la edad es al menos 18 años.
@Test
public void esAdulto_edad18_devuelveTrue() {
// Arrange
int edad = 18;
// Act
boolean resultado = verificador.esAdulto(edad);
// Assert
assertTrue(resultado, "Una persona con 18 años debería ser adulta");
}
Solución: el test es correcto, aislado y expresivo. Verifica un caso límite y usa una assertion clara con mensaje de error.
2. Profundización: usar test doubles
Imagina que tienes una clase ServicioCompra que accede a una base de datos real. Escribe un test en el que reemplaces la base de datos con un stub.
Tarea: un ServicioCompra debe calcular el total de un pedido. Para esto usa un RepositorioPrecios. Escribe un test que proporcione un stub del repositorio.
@Test
public void calculaSuma_unArticulo_precio10_esperado10() {
// Arrange
RepositorioPrecios stubRepositorio = new RepositorioPrecios() {
@Override
public double encuentraPrecios(String idArticulo) {
return 10.0;
}
};
ServicioCompra servicio = new ServicioCompra(stubRepositorio);
List<String> articulos = List.of("A1");
// Act
double suma = servicio.calculaSuma(articulos);
// Assert
assertEquals(10.0, suma, 0.001);
}
Solución: el test aísla el servicio de la base de datos. El stub siempre devuelve el precio 10.0, lo que mantiene el test rápido y determinista.
3. Enfoque para examen: clasificar tipos de test doubles
Practica asignando test doubles a escenarios. Aquí hay un pequeño ejercicio:
| Escenario | Test double apropiado |
|---|---|
| Necesitas una respuesta predefinida para una dependencia. | Stub |
| Quieres verificar si se llamó a un método específico. | Mock |
| Reemplazas una base de datos con una implementación en memoria. | Fake |
| Quieres inspeccionar después qué llamadas se realizaron. | Spy |
4. Refactorización: mejorar tests deficientes
Toma el siguiente test deficiente y transfórmalo en uno bueno y mantenible.
Versión deficiente:
@Test
public void test1() {
int x = 5;
int y = 10;
assertEquals(new Calc().add(x, y), 15);
}
Versión mejorada:
@Test
public void suma_dosNumerosPositivos_devuelveSuma() {
// Arrange
int sumando1 = 5;
int sumando2 = 10;
Calculadora calculadora = new Calculadora();
// Act
int resultado = calculadora.suma(sumando1, sumando2);
// Assert
assertEquals(15, resultado, "5 + 10 debería devolver 15");
}
Solución: el nombre del test es descriptivo, las variables hablan por sí solas, la estructura AAA es clara y la assertion incluye un mensaje de error.
5. Integración: incluir tests en el pipeline de construcción
Escribe o amplía un proyecto para que los unit tests se ejecuten automáticamente durante la construcción. En un proyecto Maven esto ocurre con mvn test, en Gradle con gradle test y en Node.js con npm test.
Tarea: configura un pipeline CI que ejecute los tests en cada commit y detenga la construcción si algo falla.
Solución: un workflow simple de GitHub Actions podría verse así:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- run: mvn test
6. Autoevaluación
Hazte estas preguntas al final:
- ¿Está cada test aislado y es determinista?
- ¿Son los nombres de los tests tan descriptivos que puedes leerlos como documentación?
- ¿Comprueban tus tests comportamiento en lugar de detalles de implementación?
- ¿Utilizas test doubles para dependencias externas?
- ¿Se ejecutan todos los tests con un único comando?
Si puedes responder sí a todos los puntos, habrás asimilado las propiedades más importantes de los buenos unit tests.
Análisis del tema
- Núcleo técnico: estructura AAA, test doubles, assertions, naming, aislamiento
- Desafíos: esfuerzo en test doubles, evitar sobre-ingeniería, tests superficiales
- Aseguramiento de calidad: detección temprana de errores, seguridad en refactorización, documentación viva
- Rentabilidad: feedback rápido, menos tiempo de inactividad, mejor mantenibilidad
- Relevancia para examen: la IHK y la práctica profesional preguntan específicamente sobre propiedades de buenos unit tests y test doubles
Fuentes principales
- https://martinfowler.com/articles/practical-test-pyramid.html
- https://junit.org/junit5/docs/current/user-guide/
- https://testing.googleblog.com



