1. Qué es TDD y qué problema resuelve en el desarrollo de software

1.1 Objetivo del tema

En este primer tema veremos qué es TDD (Test Driven Development - Desarrollo Guiado por Pruebas), por qué se usa y qué problema intenta resolver dentro del desarrollo de software. No se trata solamente de escribir pruebas, sino de usar las pruebas como una guía para diseñar y construir el código paso a paso.

Trabajaremos con Python y pytest. Como el alumno ya estudió testing y pruebas en Python, partiremos de esos conocimientos para concentrarnos en el flujo de trabajo propio de TDD: escribir primero una prueba, verla fallar, escribir el código mínimo para hacerla pasar y luego mejorar el diseño.

Objetivo práctico: comprender el sentido de TDD y ejecutar un primer ciclo rojo, verde y refactor con un ejemplo muy pequeño en Python.

1.2 Qué significa TDD

TDD viene de Test Driven Development, que en español suele traducirse como Desarrollo Guiado por Pruebas. La idea central es simple: antes de escribir el código de producción, escribimos una prueba automatizada que describe una pequeña parte del comportamiento esperado.

Luego ejecutamos la prueba. Como el comportamiento todavía no existe, la prueba debe fallar. Después escribimos el código mínimo necesario para que esa prueba pase. Finalmente, con la prueba en verde, revisamos el código y lo mejoramos sin cambiar su comportamiento.

Ese ciclo se repite muchas veces, con pasos pequeños.

1.3 El ciclo básico de TDD

El ciclo clásico de TDD tiene tres momentos:

  • Rojo: escribimos una prueba nueva y la ejecutamos. Debe fallar porque todavía no implementamos el comportamiento.
  • Verde: escribimos la solución más simple que haga pasar la prueba.
  • Refactor: mejoramos nombres, estructura y duplicación manteniendo todas las pruebas en verde.
En TDD una prueba que falla no es un problema: es una señal útil. Nos confirma que la prueba realmente está comprobando algo que todavía falta.

1.4 Qué problema resuelve TDD

Cuando se programa sin una guía clara, es común escribir demasiado código antes de comprobar si realmente cumple el requisito. Esto produce varios problemas: errores detectados tarde, funciones difíciles de modificar, diseño acoplado y miedo a cambiar código existente.

TDD ataca esos problemas reduciendo el tamaño de cada paso. En lugar de implementar una solución completa y probarla al final, avanzamos con una pregunta concreta: ¿cuál es el próximo comportamiento observable que debe tener el programa?

La prueba responde esa pregunta de manera ejecutable. Si la prueba pasa, tenemos una confirmación automática. Si más adelante rompemos ese comportamiento, la prueba lo detecta.

1.5 TDD no es escribir pruebas al final

Escribir pruebas después de terminar el código puede ser útil, pero no es TDD. En TDD la prueba aparece antes y condiciona la forma en que el código nace.

La diferencia práctica es importante. Si primero escribimos todo el código, las pruebas suelen adaptarse a la implementación ya existente. Si primero escribimos la prueba, el código se adapta al comportamiento que queremos obtener.

1.6 Requisito del ejemplo

Vamos a comenzar con un caso mínimo. Necesitamos una función que calcule el precio final de un producto aplicando un porcentaje de descuento.

Por ejemplo, si el precio es 100 y el descuento es 10, el resultado esperado es 90.

Requisito: calcular el precio final luego de aplicar un descuento porcentual.

1.7 Recordatorio del entorno de trabajo

Solo en este primer tema recordaremos los comandos básicos para preparar el entorno. A partir del tema 2 consideraremos que estos pasos ya son conocidos.

Crea una carpeta para el ejercicio y entra en ella:

mkdir tdd-descuentos
cd tdd-descuentos
Creación de la carpeta tdd-descuentos en la terminal

Crea el entorno virtual:

python -m venv .venv

Activa el entorno virtual. En Windows PowerShell:

.venv\Scripts\Activate.ps1

En Linux o macOS:

source .venv/bin/activate

Instala pytest dentro del entorno virtual:

python -m pip install pytest

Cuando tengamos pruebas escritas, las ejecutaremos con:

python -m pytest

Por facilidad, en este primer ejemplo crearemos precio.py y test_precio.py en la misma carpeta. En proyectos reales conviene separar el código de producción y las pruebas en carpetas distintas, por ejemplo src y tests. Más adelante usaremos una organización más adecuada.

1.8 Crear los archivos del ejemplo

Usaremos dos archivos:

precio.py
test_precio.py

El archivo precio.py tendrá el código de producción. El archivo test_precio.py tendrá las pruebas.

1.9 Rojo: escribir primero una prueba que falle

Antes de crear la función, escribimos una prueba que expresa el comportamiento esperado:

Archivo a crear: test_precio.py

from precio import aplicar_descuento


def test_aplicar_descuento_del_diez_por_ciento():
    resultado = aplicar_descuento(100, 10)

    assert resultado == 90

Ahora ejecutamos:

python -m pytest

La prueba debe fallar porque todavía no existe precio.py o porque todavía no existe la función aplicar_descuento. Ese fallo es correcto: estamos en la etapa roja.

1.10 Leer el fallo

Un posible mensaje de error es:

ModuleNotFoundError: No module named 'precio'

El mensaje nos dice cuál es el primer problema: la prueba intenta importar un módulo que todavía no existe. En TDD no ignoramos los fallos; los usamos como información para decidir el siguiente paso mínimo.

Mensaje de error al ejecutar la primera prueba de TDD

1.11 Verde: escribir el código mínimo

Creamos el archivo precio.py con la implementación más directa:

Archivo a crear: precio.py

def aplicar_descuento(precio, porcentaje):
    return precio - (precio * porcentaje / 100)

Ejecutamos nuevamente:

python -m pytest

Ahora la prueba debería pasar. Llegamos a la etapa verde.

Ejecución exitosa de la prueba en la etapa verde

1.12 Por qué no escribir más código todavía

Podríamos agregar validaciones, redondeos, soporte para otros tipos de datos o manejo de errores. Pero si todavía no hay una prueba que pida esos comportamientos, agregarlos ahora sería adelantarse.

En TDD buscamos que el diseño crezca por necesidad. Cada nueva prueba debe justificar el siguiente cambio en el código de producción.

1.13 Agregar un segundo ejemplo

Para tener más confianza, agregamos otra prueba:

Archivo a modificar: test_precio.py

from precio import aplicar_descuento


def test_aplicar_descuento_del_diez_por_ciento():
    resultado = aplicar_descuento(100, 10)

    assert resultado == 90


def test_aplicar_descuento_del_veinticinco_por_ciento():
    resultado = aplicar_descuento(200, 25)

            assert resultado == 150

Ejecutamos python -m pytest. Si todo sigue en verde, la función ya responde correctamente a dos ejemplos simples.

Ejecución exitosa de las pruebas con dos ejemplos de descuento

1.14 Refactor: mejorar la claridad sin cambiar el comportamiento

La implementación funciona, pero podemos hacerla un poco más clara nombrando el monto descontado:

Archivo a modificar: precio.py

def aplicar_descuento(precio, porcentaje):
    descuento = precio * porcentaje / 100
    return precio - descuento

Después de este cambio volvemos a ejecutar python -m pytest. Si las pruebas pasan, el comportamiento se mantiene. Esta es la esencia de refactorizar en TDD: mejorar el código con una red de pruebas que nos avisa si rompemos algo.

1.15 Estructura final del ejemplo

Al terminar este primer ejercicio tenemos una estructura mínima:

tdd-descuentos/
|-- precio.py
`-- test_precio.py

El tamaño del ejemplo es pequeño a propósito. En TDD conviene aprender primero el ritmo del ciclo antes de aplicarlo a problemas más grandes.

1.16 Qué ganamos con este proceso

Aunque el ejemplo sea simple, ya aparecen varias ventajas:

  • El requisito queda expresado como una prueba ejecutable.
  • Sabemos que la prueba falla antes de implementar el comportamiento.
  • Implementamos solo lo necesario para pasar la prueba actual.
  • Podemos mejorar el código con menor riesgo.
  • El ejemplo queda documentado mediante pruebas concretas.

1.17 Qué no resuelve TDD por sí solo

TDD no garantiza que el programa completo sea correcto si escribimos malas pruebas, si olvidamos casos importantes o si entendemos mal el requisito. Tampoco reemplaza las pruebas de integración, las pruebas end-to-end, la revisión de código ni el diseño consciente.

Lo que sí ofrece es una disciplina de trabajo: cambios pequeños, retroalimentación rápida y una forma concreta de conectar requisitos con código.

1.18 TDD y diseño incremental

Una consecuencia importante de TDD es que el diseño aparece de manera incremental. No intentamos imaginar toda la solución perfecta desde el inicio. Empezamos por un comportamiento pequeño, lo hacemos funcionar y luego permitimos que los siguientes ejemplos nos indiquen qué abstracciones hacen falta.

Esto no significa programar sin pensar. Significa diseñar con información real obtenida de pruebas, código ejecutable y refactorizaciones frecuentes.

1.19 Reglas prácticas para comenzar

  • Escribe una sola prueba nueva por vez.
  • Comprueba que la prueba falle por la razón esperada.
  • Implementa la solución más simple que haga pasar esa prueba.
  • No refactorices mientras la prueba está en rojo.
  • Después de refactorizar, ejecuta nuevamente toda la suite.
  • Evita probar detalles internos que podrían cambiar durante el diseño.

1.20 Ejercicio propuesto

Agrega una tercera prueba para el caso de un descuento del cero por ciento:

Archivo a modificar: test_precio.py

def test_aplicar_descuento_del_cero_por_ciento():
    resultado = aplicar_descuento(80, 0)

    assert resultado == 80

Ejecuta python -m pytest y confirma que la prueba pasa. Luego revisa si el código necesita alguna mejora. Si no hay duplicación ni nombres confusos, no es obligatorio cambiarlo.

1.21 Problemas frecuentes al empezar con TDD

  • Escribir demasiado código en verde: la solución debe cubrir la prueba actual, no todos los casos imaginables.
  • No mirar el fallo: una prueba roja debe fallar por la razón esperada. Si falla por otro motivo, primero hay que corregir esa situación.
  • Refactorizar con pruebas rotas: el refactor debe hacerse cuando la suite está en verde.
  • Probar la implementación interna: las pruebas deben describir comportamiento observable, no cada línea interna del código.
  • Dar pasos demasiado grandes: si pasan muchos minutos sin una prueba en verde, el paso probablemente es demasiado amplio.

1.22 Lista de verificación

Antes de continuar con el próximo tema, verifica lo siguiente:

  • Puedes explicar qué significa TDD.
  • Entiendes las etapas rojo, verde y refactor.
  • Creaste y activaste un entorno virtual con python -m venv .venv.
  • Instalaste pytest con python -m pip install pytest.
  • Escribiste una prueba antes del código de producción.
  • Ejecutaste la prueba con python -m pytest y verificaste que fallara.
  • Implementaste el código mínimo para hacerla pasar.
  • Refactorizaste solo después de tener las pruebas en verde.
  • Reconoces que TDD guía el diseño, no solo la verificación final.

1.23 Conclusión

TDD es una forma de desarrollar software usando pruebas automatizadas como guía. Primero expresamos un comportamiento esperado mediante una prueba, luego escribimos el código mínimo para cumplirlo y finalmente mejoramos el diseño con la seguridad de que las pruebas siguen protegiendo el comportamiento.

En este tema aplicamos el primer ciclo rojo, verde y refactor con un ejemplo pequeño en Python. En el próximo tema profundizaremos en cada etapa del ciclo para practicarlo con mayor precisión.