En este proyecto final construiremos una pequeña biblioteca Python aplicando TDD de principio a fin. El objetivo es integrar lo aprendido: elegir pruebas pequeñas, avanzar por reglas, refactorizar con la suite en verde y terminar con un módulo claro y reutilizable.
La biblioteca se llamará moneycalc y permitirá trabajar con importes, descuentos,
impuestos y redondeo de forma simple.
La biblioteca tendrá estas funciones:
aplicar_descuento: aplica un porcentaje de descuento.aplicar_impuesto: agrega un porcentaje de impuesto.calcular_total: combina descuento e impuesto.formatear_importe: devuelve un importe con dos decimales.Es una biblioteca pequeña, pero suficiente para practicar un ciclo completo.
Usaremos una estructura similar a la de un paquete real.
moneycalc-proyecto/
pyproject.toml
src/
moneycalc/
__init__.py
calculos.py
tests/
test_calculos.py
En este tema asumimos que ya sabés crear el entorno virtual, instalar pytest y
ejecutar python -m pytest.
Creamos un archivo de configuración simple.
Archivo a crear: pyproject.toml
[project]
name = "moneycalc"
version = "0.1.0"
description = "Biblioteca pequeña de cálculos monetarios creada con TDD"
requires-python = ">=3.11"
[tool.pytest.ini_options]
pythonpath = ["src"]
Esta configuración permite que las pruebas importen el paquete desde src.
Empezamos por una regla pequeña y útil.
Archivo a crear: tests/test_calculos.py
from moneycalc import aplicar_descuento
def test_aplicar_descuento_porcentual():
total = aplicar_descuento(importe=100, porcentaje=10)
assert total == 90
Ejecutamos python -m pytest. La prueba debe fallar porque todavía no existe la
función.
Creamos el paquete y la función.
Archivo a crear: src/moneycalc/__init__.py
from moneycalc.calculos import aplicar_descuento
Archivo a crear: src/moneycalc/calculos.py
def aplicar_descuento(importe, porcentaje):
return importe - importe * porcentaje / 100
Ejecutamos la prueba y llegamos a verde.
Agregamos un borde simple: si el porcentaje es cero, el importe no cambia.
Archivo a modificar: tests/test_calculos.py
def test_descuento_cero_no_modifica_el_importe():
total = aplicar_descuento(importe=100, porcentaje=0)
assert total == 100
Esta prueba probablemente pase con el código actual, pero documenta un comportamiento base.
Agregamos una validación.
Archivo a modificar: tests/test_calculos.py
import pytest
def test_no_permite_descuento_negativo():
with pytest.raises(ValueError, match="El porcentaje debe estar entre 0 y 100"):
aplicar_descuento(importe=100, porcentaje=-5)
Agregamos una función auxiliar para validar.
Archivo a modificar: src/moneycalc/calculos.py
def aplicar_descuento(importe, porcentaje):
validar_porcentaje(porcentaje)
return importe - importe * porcentaje / 100
def validar_porcentaje(porcentaje):
if porcentaje < 0 or porcentaje > 100:
raise ValueError("El porcentaje debe estar entre 0 y 100")
Ejecutamos toda la suite.
El mensaje dice que el porcentaje debe estar entre 0 y 100. Probamos el otro borde inválido.
Archivo a modificar: tests/test_calculos.py
def test_no_permite_descuento_mayor_a_cien():
with pytest.raises(ValueError, match="El porcentaje debe estar entre 0 y 100"):
aplicar_descuento(importe=100, porcentaje=120)
Agregamos la segunda función pública de la biblioteca.
Archivo a modificar: tests/test_calculos.py
from moneycalc import aplicar_descuento, aplicar_impuesto
def test_aplicar_impuesto_porcentual():
total = aplicar_impuesto(importe=100, porcentaje=21)
assert total == 121
La prueba falla hasta que exportemos e implementemos la nueva función.
Reutilizamos la validación de porcentaje.
Archivo a modificar: src/moneycalc/__init__.py
from moneycalc.calculos import aplicar_descuento, aplicar_impuesto
Archivo a modificar: src/moneycalc/calculos.py
def aplicar_impuesto(importe, porcentaje):
validar_porcentaje(porcentaje)
return importe + importe * porcentaje / 100
Ejecutamos python -m pytest. Si todo está verde, seguimos.
Ahora definimos que los importes no pueden ser negativos.
Archivo a modificar: tests/test_calculos.py
def test_no_permite_importe_negativo_en_descuento():
with pytest.raises(ValueError, match="El importe no puede ser negativo"):
aplicar_descuento(importe=-100, porcentaje=10)
Agregamos otra función auxiliar y la usamos en descuento.
Archivo a modificar: src/moneycalc/calculos.py
def aplicar_descuento(importe, porcentaje):
validar_importe(importe)
validar_porcentaje(porcentaje)
return importe - importe * porcentaje / 100
def validar_importe(importe):
if importe < 0:
raise ValueError("El importe no puede ser negativo")
Luego agregamos una prueba equivalente para aplicar_impuesto y reutilizamos la
misma validación.
La función calcular_total aplicará primero descuento y luego impuesto.
Archivo a modificar: tests/test_calculos.py
from moneycalc import aplicar_descuento, aplicar_impuesto, calcular_total
def test_calcular_total_aplica_descuento_y_luego_impuesto():
total = calcular_total(
importe=100,
descuento=10,
impuesto=21
)
assert total == 108.9
El importe 100 con 10% de descuento queda 90. Luego 21% de impuesto queda 108.9.
Exportamos e implementamos la función reutilizando las anteriores.
Archivo a modificar: src/moneycalc/__init__.py
from moneycalc.calculos import (
aplicar_descuento,
aplicar_impuesto,
calcular_total,
)
Archivo a modificar: src/moneycalc/calculos.py
def calcular_total(importe, descuento=0, impuesto=0):
total = aplicar_descuento(importe, descuento)
return aplicar_impuesto(total, impuesto)
La función principal queda expresiva porque delega reglas ya probadas.
Agregamos una regla de presentación: formatear importes con dos decimales.
Archivo a modificar: tests/test_calculos.py
from moneycalc import (
aplicar_descuento,
aplicar_impuesto,
calcular_total,
formatear_importe,
)
def test_formatea_importe_con_dos_decimales():
texto = formatear_importe(108.9)
assert texto == "108.90"
Exportamos la función y usamos formato de cadena.
Archivo a modificar: src/moneycalc/__init__.py
from moneycalc.calculos import (
aplicar_descuento,
aplicar_impuesto,
calcular_total,
formatear_importe,
)
Archivo a modificar: src/moneycalc/calculos.py
def formatear_importe(importe):
validar_importe(importe)
return f"{importe:.2f}"
Ejecutamos la suite completa para comprobar el nuevo comportamiento.
Con varias funciones públicas, conviene revisar que todas validen de forma consistente.
Archivo final: src/moneycalc/calculos.py
def aplicar_descuento(importe, porcentaje):
validar_importe(importe)
validar_porcentaje(porcentaje)
return importe - importe * porcentaje / 100
def aplicar_impuesto(importe, porcentaje):
validar_importe(importe)
validar_porcentaje(porcentaje)
return importe + importe * porcentaje / 100
def calcular_total(importe, descuento=0, impuesto=0):
total = aplicar_descuento(importe, descuento)
return aplicar_impuesto(total, impuesto)
def formatear_importe(importe):
validar_importe(importe)
return f"{importe:.2f}"
def validar_importe(importe):
if importe < 0:
raise ValueError("El importe no puede ser negativo")
def validar_porcentaje(porcentaje):
if porcentaje < 0 or porcentaje > 100:
raise ValueError("El porcentaje debe estar entre 0 y 100")
La suite debe cubrir reglas principales y bordes.
Ejecutamos la suite completa.
python -m pytest
Luego revisamos el estado del trabajo.
git status
git diff
Si todo está en verde y los cambios son coherentes, podemos crear un commit.
git add pyproject.toml src/moneycalc tests/test_calculos.py
git commit -m "Crea biblioteca moneycalc con TDD"
El proyecto final se considera completo cuando:
python -m pytest.moneycalc.Si querés seguir practicando, agregá estas funcionalidades con TDD.
formatear_importe, por ejemplo "$ 108.90".dividir_en_cuotas con redondeo a dos decimales.calcular_subtotal desde una lista de ítems.TDD no es solamente escribir pruebas antes del código. Es una forma de pensar y construir: convertir incertidumbre en ejemplos ejecutables, avanzar en pasos pequeños, diseñar desde el comportamiento y mejorar el código con una red de seguridad.
Con este proyecto final integraste el ciclo rojo, verde y refactor en una biblioteca Python pequeña, verificable y lista para extenderse con nuevas reglas.