11. Desarrollo guiado por pruebas (TDD)

El Desarrollo Guiado por Pruebas (Test-Driven Development o TDD) es una práctica de desarrollo de software que invierte el orden tradicional de la programación. En lugar de escribir el código de producción primero y las pruebas después (si es que se escriben), TDD dicta que las pruebas deben escribirse antes que el código que las hará pasar. Esta disciplina es fundamental en Extreme Programming (XP) por su impacto directo en la calidad y el diseño del software.

11.1. Filosofía: primero las pruebas, luego el código

La idea central de TDD es que una prueba automatizada define el comportamiento esperado de una pequeña pieza de funcionalidad antes de que esta exista. La prueba actúa como una especificación ejecutable. Al escribir la prueba primero, el desarrollador se obliga a pensar en los requisitos y el comportamiento deseado desde la perspectiva del "cliente" de ese código (otra función, otro módulo, etc.).

Este enfoque tiene varias implicaciones profundas:

  • Garantiza la "probabilidad": Si escribes el código primero, puedes, consciente o inconscientemente, diseñarlo de una manera que sea difícil de probar. Al escribir la prueba primero, te aseguras de que el código resultante será, por definición, "probable" (testable).
  • Clarifica los requisitos: No puedes escribir una prueba para algo que no entiendes. El acto de escribir la prueba te fuerza a clarificar qué debe hacer exactamente la funcionalidad, incluyendo casos límite y de error.
  • Evita el sobre-diseño: Solo escribes el código estrictamente necesario para que la prueba pase. Esto combate la tendencia a añadir funcionalidades "por si acaso" y mantiene el diseño simple, un valor clave de XP.

11.2. Ciclo Red → Green → Refactor

TDD se desarrolla en un ciclo muy corto y repetitivo conocido como "Red-Green-Refactor". Cada ciclo dura solo unos minutos y se enfoca en una pequeña pieza de comportamiento.

  1. Red (Rojo): Escribe una prueba automatizada que falla. Falla porque el código de la funcionalidad que está probando aún no existe o no implementa el comportamiento esperado. Ver la prueba fallar (en rojo) es crucial para confirmar que la prueba funciona correctamente y que no pasará por accidente.
  2. Green (Verde): Escribe la cantidad mínima de código de producción necesaria para que la prueba que acabas de escribir pase (se ponga en verde). En este paso, el objetivo no es escribir código elegante o eficiente, sino simplemente hacer que la prueba pase. La solución puede ser "fea" o incompleta, pero debe ser correcta para el caso que la prueba cubre.
  3. Refactor (Refactorizar): Una vez que la prueba está en verde y tienes la seguridad de que el comportamiento funciona, puedes mejorar la estructura interna del código que acabas de escribir. Esto incluye eliminar duplicación, mejorar la claridad de los nombres, simplificar la lógica, etc. La clave es que la refactorización no debe cambiar el comportamiento observable del código; después de refactorizar, todas las pruebas deben seguir pasando.

Este ciclo se repite para cada nueva pieza de funcionalidad, construyendo gradualmente una base de código robusta y cubierta por un conjunto completo de pruebas automáticas.

11.3. Beneficios del TDD

La adopción de TDD, aunque requiere disciplina, aporta beneficios significativos al proceso de desarrollo:

  • Red de seguridad: El conjunto de pruebas automáticas actúa como una red de seguridad que permite a los desarrolladores hacer cambios y refactorizar con confianza. Si un cambio rompe algo, las pruebas lo detectarán inmediatamente.
  • Mejora del diseño: TDD fomenta un diseño de bajo acoplamiento y alta cohesión, ya que el código difícil de probar suele ser un síntoma de un mal diseño.
  • Documentación viva: El conjunto de pruebas sirve como una documentación precisa y siempre actualizada del comportamiento del sistema. Para entender qué hace un módulo, un desarrollador puede leer sus pruebas.
  • Reducción de bugs: Al probar cada pieza de funcionalidad desde el principio, se reduce drásticamente la cantidad de defectos que llegan a fases posteriores del desarrollo o a producción.
  • Desarrollo enfocado: El ciclo TDD mantiene a los desarrolladores enfocados en un requisito a la vez, evitando distracciones y asegurando un progreso constante.

11.4. Ejemplo simple en Python

Veamos un ejemplo de TDD para crear una función simple que suma dos números en Python, usando el framework de pruebas `unittest`.

Paso 1: Red (Escribir una prueba que falla)

Primero, creamos un archivo de prueba `test_calculadora.py` y escribimos una prueba para una función `sumar` que aún no existe.


import unittest
# Aún no importamos la calculadora porque no existe

class TestCalculadora(unittest.TestCase):

    def test_sumar_dos_numeros(self):
        """Prueba que la función sumar puede sumar dos números positivos."""
        from calculadora import sumar  # Intentamos importar la función
        resultado = sumar(2, 3)
        self.assertEqual(resultado, 5)

if __name__ == '__main__':
    unittest.main()

Si ejecutamos esta prueba, fallará con un `ModuleNotFoundError` (o `ImportError`) porque `calculadora.py` no existe. Esto es nuestro estado "Red".

Paso 2: Green (Escribir el código mínimo para que pase)

Ahora, creamos el archivo `calculadora.py` y escribimos el código más simple posible para que la prueba pase.


# calculadora.py

def sumar(a, b):
    return a + b

Si volvemos a ejecutar `test_calculadora.py`, la prueba ahora pasará. Hemos alcanzado el estado "Green".

Paso 3: Refactor (Refactorizar el código)

En este caso, la función `sumar` es tan simple que no hay mucho que refactorizar. Sin embargo, podríamos pensar en mejorarla. Por ejemplo, ¿qué pasa si queremos añadir un docstring para explicar lo que hace?


# calculadora.py (refactorizado)

def sumar(a, b):
    """Suma dos números y devuelve el resultado."""
    return a + b

Después de esta refactorización, volvemos a ejecutar las pruebas. Deben seguir pasando, lo que nos confirma que no hemos roto nada. Ahora el ciclo está completo y podemos proceder a escribir la siguiente prueba para un nuevo comportamiento (por ejemplo, sumar números negativos, sumar cero, etc.).