8. Triangulación: usar nuevos ejemplos para generalizar el código

8.1 Objetivo del tema

En este tema veremos una técnica muy útil en TDD: triangulación. Triangular consiste en agregar nuevos ejemplos para obligar al código a pasar de una solución específica a una solución más general.

Esta técnica ayuda cuando una primera implementación puede ser demasiado simple, como devolver un valor fijo. En lugar de generalizar por intuición, dejamos que otra prueba nos obligue a hacerlo.

Objetivo práctico: construir una función de puntos de fidelidad agregando ejemplos que empujen el diseño hacia una fórmula general.

8.2 Qué significa triangular

Triangular es usar dos o más ejemplos para descubrir la regla general. Con un solo ejemplo, muchas implementaciones pueden pasar la prueba. Con varios ejemplos bien elegidos, las soluciones falsas empiezan a fallar.

La triangulación evita saltar demasiado pronto a una abstracción. Primero dejamos que los ejemplos nos muestren qué necesita el código.

8.3 Requisito de trabajo

Crearemos una función para calcular puntos de fidelidad de una tienda:

Por cada 10 pesos de compra, el cliente recibe 1 punto.

Por ahora ignoraremos promociones, categorías de cliente y fechas especiales. Solo practicaremos la regla base.

8.4 Archivos del ejemplo

Usaremos estos archivos:

src/tienda/fidelidad.py
tests/test_fidelidad.py

El archivo de producción tendrá la función calcular_puntos.

8.5 Primer ejemplo

Comenzamos con una compra de 100 pesos. Según el requisito, debe otorgar 10 puntos.

Archivo a crear: tests/test_fidelidad.py

from tienda.fidelidad import calcular_puntos


def test_calcular_puntos_para_compra_de_cien_pesos():
    assert calcular_puntos(100) == 10

Ejecutamos:

python -m pytest

La prueba debe fallar porque todavía no existe la función.

8.6 Verde con una solución específica

Creamos la función con la implementación mínima.

Archivo a crear: src/tienda/fidelidad.py

def calcular_puntos(total_compra):
    return 10

Ejecutamos python -m pytest. La prueba pasa, pero el código solo funciona para el ejemplo actual.

8.7 Por qué no generalizar todavía

Podríamos escribir directamente total_compra // 10, pero en este momento solo tenemos un ejemplo. TDD permite empezar con una solución específica y dejar que otro ejemplo fuerce la generalización.

Este enfoque ayuda a no escribir lógica que todavía no fue comprobada por pruebas.

8.8 Segundo ejemplo para triangular

Agregamos una compra de 50 pesos. Debería otorgar 5 puntos.

Archivo a modificar: tests/test_fidelidad.py

from tienda.fidelidad import calcular_puntos


def test_calcular_puntos_para_compra_de_cien_pesos():
    assert calcular_puntos(100) == 10


def test_calcular_puntos_para_compra_de_cincuenta_pesos():
    assert calcular_puntos(50) == 5

Ejecutamos python -m pytest. La segunda prueba debe fallar porque la función devuelve siempre 10.

8.9 Generalizar con la nueva información

Ahora sí tenemos dos ejemplos que sugieren una regla. Podemos implementar una fórmula.

Archivo a modificar: src/tienda/fidelidad.py

def calcular_puntos(total_compra):
    return total_compra // 10

Ejecutamos:

python -m pytest

Si ambas pruebas pasan, el código ya no depende de un valor fijo.

8.10 Tercer ejemplo: compra que no divide exacto

Necesitamos decidir qué pasa con una compra de 95 pesos. Si se otorga 1 punto por cada 10 pesos completos, el resultado debe ser 9.

Archivo a modificar: tests/test_fidelidad.py

from tienda.fidelidad import calcular_puntos


def test_calcular_puntos_para_compra_de_cien_pesos():
    assert calcular_puntos(100) == 10


def test_calcular_puntos_para_compra_de_cincuenta_pesos():
    assert calcular_puntos(50) == 5


def test_calcular_puntos_ignora_fraccion_menor_a_diez_pesos():
    assert calcular_puntos(95) == 9

Ejecutamos python -m pytest. Con la división entera, esta prueba debería pasar.

8.11 Cuando un ejemplo confirma la regla

La prueba de 95 puede pasar sin cambios en producción. Aun así, es útil porque documenta una decisión: los pesos restantes que no llegan a 10 no generan puntos.

La triangulación no solo sirve para hacer fallar código. También sirve para aclarar reglas ambiguas.

8.12 Cuarto ejemplo: compra menor a diez

Agregamos una compra de 9 pesos. Debería otorgar 0 puntos.

Archivo a modificar: tests/test_fidelidad.py

from tienda.fidelidad import calcular_puntos


def test_calcular_puntos_para_compra_menor_a_diez_pesos():
    assert calcular_puntos(9) == 0

Esta prueba puede agregarse junto con las anteriores. Ejecutamos python -m pytest para confirmar que la regla se mantiene.

8.13 Refactor: parametrizar ejemplos

Como todos los ejemplos prueban la misma regla, podemos usar parametrización.

Archivo a modificar: tests/test_fidelidad.py

import pytest

from tienda.fidelidad import calcular_puntos


@pytest.mark.parametrize(
    "total_compra, puntos_esperados",
    [
        (100, 10),
        (50, 5),
        (95, 9),
        (9, 0),
    ],
)
def test_calcular_puntos_por_cada_diez_pesos(total_compra, puntos_esperados):
    assert calcular_puntos(total_compra) == puntos_esperados

Ejecutamos python -m pytest. Si todo pasa, la prueba quedó más compacta sin cambiar comportamiento.

8.14 Refactor: nombrar la regla

Podemos dar nombre al divisor para que el código explique mejor la regla.

Archivo a modificar: src/tienda/fidelidad.py

PESOS_POR_PUNTO = 10


def calcular_puntos(total_compra):
    return total_compra // PESOS_POR_PUNTO

Ejecutamos nuevamente la suite.

python -m pytest

8.15 Triangulación contra implementación falsa

El primer código return 10 era una implementación falsa útil para pasar el primer ejemplo. La segunda prueba la hizo insuficiente. Esa es la función de la triangulación: encontrar ejemplos que obliguen al código a expresar la regla real.

No toda implementación falsa es mala. Es mala si permanece cuando las pruebas ya exigen una generalización.

8.16 Elegir buenos ejemplos

Para triangular bien, conviene elegir ejemplos que aporten información diferente:

  • Un caso común: 100 pesos devuelve 10.
  • Otro caso común con distinto resultado: 50 devuelve 5.
  • Un caso que no divide exacto: 95 devuelve 9.
  • Un caso por debajo del mínimo: 9 devuelve 0.

Ejemplos variados ayudan a descubrir la regla real con menos pruebas.

8.17 Qué evitar al triangular

  • Agregar muchos ejemplos sin intención: cada ejemplo debería aclarar una parte de la regla.
  • Generalizar antes de necesitarlo: puede crear código más complejo que el problema actual.
  • Usar datos repetidos: dos ejemplos equivalentes no aportan mucha información nueva.
  • Ignorar casos borde: suelen definir la regla con precisión.
  • Parametrizar demasiado pronto: al principio, pruebas separadas pueden ser más fáciles de leer.

8.18 Comparación con baby steps

Baby steps se concentra en el tamaño del cambio. Triangulación se concentra en elegir nuevos ejemplos para guiar la generalización.

En la práctica se usan juntos: damos pasos pequeños y seleccionamos ejemplos que obligan al código a evolucionar.

8.19 Ejercicio propuesto

Agrega una nueva regla usando triangulación:

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

Escribe primero un ejemplo con -10. Luego piensa si hace falta otro ejemplo negativo, como -1, para aclarar la regla. Ejecuta python -m pytest después de cada paso.

8.20 Lista de verificación

Antes de continuar, verifica lo siguiente:

  • Comenzaste con una prueba concreta.
  • Aceptaste una implementación específica solo mientras era suficiente.
  • Agregaste un segundo ejemplo para forzar la generalización.
  • Usaste casos borde para aclarar la regla.
  • Parametrizaste cuando varios ejemplos ya expresaban el mismo comportamiento.
  • Ejecutaste python -m pytest después de cada cambio importante.

8.21 Conclusión

En este tema usamos triangulación para pasar de una implementación específica a una regla general. Cada nuevo ejemplo aportó información: primero un segundo valor, luego un caso no exacto y finalmente un caso menor al mínimo.

En el próximo tema trabajaremos sobre nombres de pruebas, para que cada prueba comunique claramente el comportamiento que describe.