7. Baby steps: avanzar con cambios pequeños y verificables

7.1 Objetivo del tema

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.

Objetivo práctico: desarrollar una regla de costo de envío con cambios mínimos, ejecutando pytest después de cada avance importante.

7.2 Qué son los baby steps

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:

  • Escribir una prueba muy concreta.
  • Ejecutarla y verla fallar.
  • Hacer el cambio mínimo para que pase.
  • Ejecutar nuevamente.
  • Refactorizar solo si el código lo necesita.

7.3 Requisito de trabajo

Crearemos una función que calcule el costo de envío de una compra. El primer requisito será pequeño:

Si el total de la compra es menor que 100, el costo de envío es 10.

No agregaremos todavía envío gratis, zonas, peso ni descuentos. Cada regla llegará con su propia prueba.

7.4 Archivos del ejemplo

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.

7.5 Primer paso: escribir una prueba mínima

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.

7.6 Segundo paso: crear el módulo vacío

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.

7.7 Tercer paso: crear la función

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.

7.8 Por qué no agregar más reglas todavía

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.

7.9 Nueva regla pequeña

Agregamos una segunda regla:

Si el total de la compra es 100 o más, el envío es gratis.

Primero escribimos una prueba concreta.

7.10 Escribir la segunda prueba

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.

7.11 Implementar lo mínimo para la nueva regla

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.

7.12 Verificar el borde inferior

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.

7.13 Cuando un paso no obliga a cambiar producción

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.

7.14 Refactor pequeño: nombrar constantes

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.

7.15 Refactor pequeño: nombrar el envío gratis

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.

7.16 Refactor de pruebas con parametrización

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.

7.17 Cómo saber si el paso es demasiado grande

Un paso suele ser demasiado grande si:

  • Pasas mucho tiempo sin ejecutar las pruebas.
  • Modificas producción y pruebas en muchas partes a la vez.
  • Cuando falla la suite, no sabes qué cambio produjo el fallo.
  • Agregas varias reglas de negocio en una sola implementación.

Cuando eso ocurre, conviene retroceder mentalmente y dividir el trabajo en ejemplos más pequeños.

7.18 Errores frecuentes

  • Escribir muchas pruebas antes de implementar: puede generar demasiadas causas de fallo al mismo tiempo.
  • Resolver todo el problema de una vez: dificulta aprender del feedback de cada prueba.
  • No ejecutar pytest entre cambios: se pierde la ventaja de retroalimentación rápida.
  • Refactorizar y agregar reglas al mismo tiempo: mezcla dos tipos de cambio diferentes.
  • Ignorar los casos borde: los límites suelen revelar errores en condiciones como > y >=.

7.19 Ejercicio propuesto

Agrega una nueva regla con baby steps:

Si el total de la compra es negativo, debe lanzarse ValueError.

Hazlo en pasos pequeños:

  • Escribe una prueba con pytest.raises(ValueError).
  • Ejecuta python -m pytest y verifica que falle.
  • Agrega la condición mínima en calcular_envio.
  • Ejecuta la suite completa.
  • Refactoriza solo si el código queda más claro.

7.20 Lista de verificación

Antes de continuar, verifica lo siguiente:

  • Escribiste una prueba pequeña por vez.
  • Ejecutaste python -m pytest después de cada cambio relevante.
  • Implementaste solo la regla que la prueba pedía.
  • Agregaste casos de borde sin cambiar producción innecesariamente.
  • Refactorizaste con la suite en verde.
  • Reconoces cuándo un paso es demasiado grande.

7.21 Conclusión

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.