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.
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.
El ciclo clásico de TDD tiene tres momentos:
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
Aunque el ejemplo sea simple, ya aparecen varias ventajas:
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.
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.
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.
Antes de continuar con el próximo tema, verifica lo siguiente:
python -m venv .venv.pytest con python -m pip install pytest.python -m pytest y verificaste que fallara.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.