En este tema practicaremos una disciplina muy importante dentro de TDD: avanzar con pasos pequeños, también llamados baby steps. Un paso pequeño es un cambio que podemos entender, ejecutar y corregir rápidamente.
Cuando los pasos son demasiado grandes, una falla puede venir de muchas causas. Cuando los pasos son pequeños, cada ejecución de la suite nos da retroalimentación clara.
Los baby steps son avances deliberadamente pequeños. No buscan ir lento por ir lento, sino mantener el control técnico del cambio.
En TDD un paso pequeño suele tener esta forma:
Crearemos una función que calcule el costo de envío de una compra. El primer requisito será pequeño:
No agregaremos todavía envío gratis, zonas, peso ni descuentos. Cada regla llegará con su propia prueba.
Usaremos dos archivos:
src/tienda/envio.py
tests/test_envio.py
El módulo envio.py contendrá la función de producción. El archivo test_envio.py contendrá las pruebas.
Elegimos un ejemplo concreto: una compra de 50 debe pagar envío de 10.
Archivo a crear: tests/test_envio.py
from tienda.envio import calcular_envio
def test_envio_cuesta_diez_si_total_es_menor_a_cien():
assert calcular_envio(50) == 10
Ejecutamos:
python -m pytest
La prueba debe fallar porque el módulo o la función todavía no existen.
Si el fallo indica que falta tienda.envio, el siguiente paso mínimo es crear el archivo.
Archivo a crear: src/tienda/envio.py
Crea el archivo vacío y ejecuta nuevamente:
python -m pytest
El error debería cambiar. Ese cambio confirma que avanzamos un paso.
Ahora el fallo probablemente indique que no se puede importar calcular_envio. Agregamos la función con la implementación más pequeña.
Archivo a modificar: src/tienda/envio.py
def calcular_envio(total):
return 10
Ejecutamos:
python -m pytest
La prueba debería pasar. No intentamos resolver todo el negocio; resolvimos el comportamiento actual.
Sabemos que probablemente existirá una regla de envío gratis, pero todavía no hay una prueba que la pida. Agregarla ahora sería adelantarse.
Con baby steps, el código crece cuando una prueba lo exige. Esto evita diseñar de más y permite detectar errores rápidamente.
Agregamos una segunda regla:
Primero escribimos una prueba concreta.
Archivo a modificar: tests/test_envio.py
from tienda.envio import calcular_envio
def test_envio_cuesta_diez_si_total_es_menor_a_cien():
assert calcular_envio(50) == 10
def test_envio_es_gratis_si_total_es_cien_o_mas():
assert calcular_envio(100) == 0
Ejecutamos python -m pytest. La segunda prueba debe fallar porque la función devuelve siempre 10.
Ahora la prueba justifica agregar una condición.
Archivo a modificar: src/tienda/envio.py
def calcular_envio(total):
if total >= 100:
return 0
return 10
Ejecutamos:
python -m pytest
Si ambas pruebas pasan, volvimos a verde.
La regla tiene un límite: 100. Ya probamos exactamente 100, pero podemos agregar un ejemplo justo antes del límite.
Archivo a modificar: tests/test_envio.py
from tienda.envio import calcular_envio
def test_envio_cuesta_diez_si_total_es_menor_a_cien():
assert calcular_envio(50) == 10
def test_envio_cuesta_diez_si_total_es_noventa_y_nueve():
assert calcular_envio(99) == 10
def test_envio_es_gratis_si_total_es_cien_o_mas():
assert calcular_envio(100) == 0
Ejecutamos python -m pytest. Esta prueba debería pasar con la implementación actual.
La prueba con 99 puede pasar sin tocar el código. Aun así, puede ser valiosa porque documenta el comportamiento justo antes del límite.
Los baby steps no siempre producen código nuevo. A veces producen mejor documentación ejecutable.
Con la suite en verde, podemos mejorar nombres internos.
Archivo a modificar: src/tienda/envio.py
TOTAL_MINIMO_ENVIO_GRATIS = 100
COSTO_ENVIO = 10
def calcular_envio(total):
if total >= TOTAL_MINIMO_ENVIO_GRATIS:
return 0
return COSTO_ENVIO
Ejecutamos python -m pytest. El comportamiento debe mantenerse.
También podemos nombrar el valor 0 para que la regla sea más expresiva.
Archivo a modificar: src/tienda/envio.py
TOTAL_MINIMO_ENVIO_GRATIS = 100
COSTO_ENVIO = 10
ENVIO_GRATIS = 0
def calcular_envio(total):
if total >= TOTAL_MINIMO_ENVIO_GRATIS:
return ENVIO_GRATIS
return COSTO_ENVIO
Ejecutamos nuevamente python -m pytest.
Las pruebas pueden quedar más compactas usando varios ejemplos parametrizados.
Archivo a modificar: tests/test_envio.py
import pytest
from tienda.envio import calcular_envio
@pytest.mark.parametrize(
"total, esperado",
[
(50, 10),
(99, 10),
(100, 0),
],
)
def test_calcular_envio(total, esperado):
assert calcular_envio(total) == esperado
Ejecutamos python -m pytest. Si todo sigue en verde, el refactor fue seguro.
Un paso suele ser demasiado grande si:
Cuando eso ocurre, conviene retroceder mentalmente y dividir el trabajo en ejemplos más pequeños.
> y >=.Agrega una nueva regla con baby steps:
Hazlo en pasos pequeños:
pytest.raises(ValueError).python -m pytest y verifica que falle.calcular_envio.Antes de continuar, verifica lo siguiente:
python -m pytest después de cada cambio relevante.En este tema practicamos baby steps con una regla de costo de envío. Cada avance fue pequeño: escribir una prueba, verla fallar, implementar lo mínimo, ejecutar la suite y refactorizar cuando había una mejora clara.
En el próximo tema veremos triangulación: cómo usar nuevos ejemplos para evitar soluciones demasiado específicas y llevar el código hacia una implementación más general.