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.
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.
Crearemos una función para calcular puntos de fidelidad de una tienda:
Por ahora ignoraremos promociones, categorías de cliente y fechas especiales. Solo practicaremos la regla base.
Usaremos estos archivos:
src/tienda/fidelidad.py
tests/test_fidelidad.py
El archivo de producción tendrá la función calcular_puntos.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
Para triangular bien, conviene elegir ejemplos que aporten información diferente:
100 pesos devuelve 10.50 devuelve 5.95 devuelve 9.9 devuelve 0.Ejemplos variados ayudan a descubrir la regla real con menos pruebas.
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.
Agrega una nueva regla usando triangulación:
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.
Antes de continuar, verifica lo siguiente:
python -m pytest después de cada cambio importante.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.