Skip to content
IRC-CodingIRC-Coding
Unit TestsTestdoublesAAA Arrange Act AssertTests AisladosTests RápidosTests SignificativosTests Mantenibles

Características de Unit Tests efectivos

Unit Tests aislados, rápidos y mantenibles. Testdoubles, patrón AAA y mejores prácticas.

S

schutzgeist

13 min read
Características de Unit Tests efectivos

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_expectedResult hace 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

  1. 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.

  2. 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.

  3. Convenciones de nomenclatura Un buen nombre de test describe el escenario y el resultado esperado. Patrones como methodName_condition_expectedResult convierten los tests en documentación legible.

  4. Assertions Las assertions son las verificaciones al final de un test. Deben ser precisas y proporcionar un mensaje informativo cuando fallan.

  5. 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.

  6. Aislamiento Un unit test verifica exactamente una unidad. Las bases de datos, redes, archivos u otros servicios se reemplazan con test doubles.

  7. 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.

  8. Independencia Los tests no deben depender unos de otros. Cada test crea sus propios datos y no deja estados que afecten al siguiente test.

  9. 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.

  10. 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?

Un unit test verifica la unidad más pequeña y testeable de un programa, generalmente un método o una clase, aislado de dependencias externas.

2. ¿Qué significa AAA en unit tests?

AAA representa Arrange, Act, Assert. Primero preparas los datos de prueba, luego ejecutas el método a probar y finalmente verificas el resultado.

3. ¿Qué hace que un unit test sea correcto?

Un unit test correcto verifica exactamente el comportamiento que debe documentar y se basa en supuestos válidos. Simultáneamente es una especificación de la unidad.

4. ¿Qué significa aislamiento en unit tests?

El aislamiento significa que un unit test no requiere sistemas externos como bases de datos, redes o sistemas de archivos. Las dependencias externas se reemplazan con test doubles.

5. ¿Qué es un test double?

Un test double es un reemplazo para una dependencia real en una prueba. Incluye stubs, mocks, fakes y spies.

6. ¿Cuál es la diferencia entre un stub y un mock?

Un stub proporciona respuestas predefinidas para las dependencias. Un mock además verifica que ciertos métodos fueron llamados con los parámetros esperados.

7. ¿Qué es un fake?

Un fake es una implementación simplificada pero funcional de una dependencia. Un repositorio en memoria en lugar de una base de datos real es un ejemplo típico.

8. ¿Qué es un spy?

Un spy es un test double que registra las llamadas a métodos sin simular completamente el comportamiento. Ayuda a verificar las interacciones más adelante.

9. ¿Por qué los unit tests deben ser rápidos?

Los unit tests rápidos se ejecutan frecuentemente durante el desarrollo y en la pipeline de CI. Los tests lentos ralentizan el flujo de trabajo y suelen ser ignorados.

10. ¿Qué significa expresivo en relación a los tests?

Un test expresivo tiene un nombre claro, una estructura comprensible y mensajes de error precisos. Si falla, sabes inmediatamente qué está mal.

11. ¿Cuál es una buena convención de nombres para unit tests?

Un patrón popular es nombreMetodo_condicion_resultadoEsperado, por ejemplo calcularDescuento_clienteNuevo_articuloEstandar_esperaCeroPorciento. Alternativamente funciona deberiaX_cuandoY.

12. ¿Qué es una assertion?

Una assertion es una verificación que compara el resultado real con el resultado esperado. Los ejemplos incluyen assertEquals, assertTrue o assertThrows.

13. ¿Qué son números mágicos en tests?

Los números mágicos son valores codificados sin contexto explicativo. En tests debes usar constantes o variables con nombres significativos para dejar clara la intención.

14. ¿Por qué los tests deben ser independientes entre sí?

Los tests independientes no dejan estado para otros tests y se ejecutan en cualquier orden. Esto hace la suite estable y previene efectos secundarios difíciles de detectar.

15. ¿Qué significa determinista en unit tests?

Un test determinista produce el mismo resultado cada vez que se ejecuta con las mismas entradas. La aleatoriedad, la hora del sistema y los estados externos no tienen lugar en unit tests.

16. ¿Qué se entiende por mantenibilidad de tests?

Los tests mantenibles están bien estructurados, evitan duplicación, usan nombres claros y resisten refactorings sin cambios. Son tan importantes como el código de producción.

17. ¿Cuál es la diferencia entre unit tests, tests de integración y tests E2E?

Los unit tests verifican unidades aisladas. Los tests de integración verifican la interacción de múltiples unidades. Los tests end-to-end verifican la aplicación completa desde la perspectiva del usuario.

18. ¿Qué son casos límite?

Los casos límite son situaciones extremas como entradas vacías, valores nulos, valores máximos o secuencias inesperadas. Los buenos tests consideran estos explícitamente, no solo el caso normal.

19. ¿Qué es un happy path?

El happy path describe el caso estándar donde todo funciona como se espera. Los tests deben cubrir el happy path además de casos de error y casos límite.

20. ¿Qué es una test factory?

Una test factory es un método auxiliar que crea objetos de prueba con valores por defecto sensatos. Evita duplicación y mantiene los tests legibles.

21. ¿Qué es seguridad de refactoring en tests?

Los tests son seguros para refactoring cuando verifican comportamiento y no detalles internos de implementación. Permanecen válidos cuando el código de producción se reestructura.

22. ¿Qué es un test flaky?

Un test flaky a veces pasa y a veces falla sin que el código haya cambiado. Los tests flaky generalmente indican comportamiento no determinista o dependencias externas.

23. ¿Qué es cobertura de código?

La cobertura de código indica qué porcentaje del código fuente es ejecutado por los tests. Pero una cobertura alta por sí sola no dice nada sobre la calidad de los tests.

24. ¿Por qué un test debe tener una única responsabilidad?

Cuando un test verifica múltiples comportamientos independientes, no está claro cuál suposición fue incorrecta si falla. Múltiples assertions están permitidas siempre que estén relacionadas.

25. ¿Qué significa fácil de ejecutar en unit tests?

Un unit test debe ser ejecutable con un comando, no requerir preparación manual y ser determinista. Debe funcionar en cualquier lugar donde esté presente el código.

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:

EscenarioTest 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

  1. https://martinfowler.com/articles/practical-test-pyramid.html
  2. https://junit.org/junit5/docs/current/user-guide/
  3. https://testing.googleblog.com
Volver al blog
Share:

Entradas relacionadas