12. Diseño incremental de funciones puras usando TDD

12.1 Objetivo del tema

En este tema aplicaremos TDD al diseño incremental de funciones puras. Una función pura es especialmente cómoda para practicar TDD porque no depende de archivos, base de datos, red, tiempo ni estado externo.

Construiremos una función que convierte un puntaje numérico en una calificación textual. La función crecerá ejemplo por ejemplo.

Objetivo práctico: diseñar una función pura con pruebas pequeñas, agregando reglas de calificación de manera incremental.

12.2 Qué es una función pura

Una función pura cumple dos condiciones principales:

  • Para la misma entrada, siempre devuelve la misma salida.
  • No produce efectos secundarios observables, como escribir archivos, modificar variables globales o llamar servicios externos.

Por ejemplo:

def doble(numero):
    return numero * 2

12.3 Por qué son buenas para TDD

Las funciones puras son fáciles de probar porque solo necesitamos preparar entradas y verificar salidas. No hace falta configurar dependencias externas.

Esto permite concentrarse en el ciclo rojo, verde y refactor sin ruido adicional.

12.4 Requisito de trabajo

Crearemos una función llamada obtener_calificacion. Recibirá un puntaje entre 0 y 100 y devolverá una letra.

Si el puntaje es 90 o más, la calificación es "A".

Empezaremos solo con esta regla. Las demás calificaciones aparecerán mediante nuevas pruebas.

12.5 Archivos del ejemplo

Usaremos estos archivos:

src/academico/calificaciones.py
tests/test_calificaciones.py

La función de producción estará en calificaciones.py.

12.6 Primera prueba: calificación A

Escribimos una prueba para un puntaje claramente dentro del rango de A.

Archivo a crear: tests/test_calificaciones.py

from academico.calificaciones import obtener_calificacion


def test_devuelve_a_si_puntaje_es_noventa_o_mas():
    assert obtener_calificacion(95) == "A"

Ejecutamos:

python -m pytest

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

12.7 Verde con implementación mínima

Creamos la función con lo mínimo para pasar la prueba.

Archivo a crear: src/academico/calificaciones.py

def obtener_calificacion(puntaje):
    return "A"

Ejecutamos python -m pytest. La prueba debería pasar.

12.8 Agregar una segunda regla

Ahora agregamos la calificación B:

Si el puntaje es 80 o más y menor que 90, la calificación es "B".

Escribimos una prueba nueva.

12.9 Prueba para B

Archivo a modificar: tests/test_calificaciones.py

from academico.calificaciones import obtener_calificacion


def test_devuelve_a_si_puntaje_es_noventa_o_mas():
    assert obtener_calificacion(95) == "A"


def test_devuelve_b_si_puntaje_esta_entre_ochenta_y_ochenta_y_nueve():
    assert obtener_calificacion(85) == "B"

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

12.10 Generalizar lo mínimo

Agregamos la condición necesaria.

Archivo a modificar: src/academico/calificaciones.py

def obtener_calificacion(puntaje):
    if puntaje >= 90:
        return "A"
    return "B"

Ejecutamos:

python -m pytest

Las pruebas para A y B deberían pasar.

12.11 Agregar C

Sumamos una tercera regla:

Si el puntaje es 70 o más y menor que 80, la calificación es "C".

Primero escribimos la prueba.

12.12 Prueba para C

Archivo a modificar: tests/test_calificaciones.py

from academico.calificaciones import obtener_calificacion


def test_devuelve_a_si_puntaje_es_noventa_o_mas():
    assert obtener_calificacion(95) == "A"


def test_devuelve_b_si_puntaje_esta_entre_ochenta_y_ochenta_y_nueve():
    assert obtener_calificacion(85) == "B"


def test_devuelve_c_si_puntaje_esta_entre_setenta_y_setenta_y_nueve():
    assert obtener_calificacion(75) == "C"

Ejecutamos python -m pytest. La nueva prueba debe fallar.

12.13 Implementar C

La nueva prueba justifica otra condición.

Archivo a modificar: src/academico/calificaciones.py

def obtener_calificacion(puntaje):
    if puntaje >= 90:
        return "A"
    if puntaje >= 80:
        return "B"
    return "C"

Ejecutamos python -m pytest. Si todo pasa, seguimos en verde.

12.14 Completar la regla de desaprobado

Agregamos una regla final para puntajes menores que 70:

Si el puntaje es menor que 70, la calificación es "D".

Esta regla cubrirá los puntajes que no entran en A, B o C.

12.15 Prueba para D

Archivo a modificar: tests/test_calificaciones.py

from academico.calificaciones import obtener_calificacion


def test_devuelve_d_si_puntaje_es_menor_que_setenta():
    assert obtener_calificacion(60) == "D"

Agrega esta prueba junto con las anteriores y ejecuta python -m pytest. Debe fallar porque la función devuelve "C" para todo puntaje menor que 80.

12.16 Implementación completa

Completamos la función:

Archivo a modificar: src/academico/calificaciones.py

def obtener_calificacion(puntaje):
    if puntaje >= 90:
        return "A"
    if puntaje >= 80:
        return "B"
    if puntaje >= 70:
        return "C"
    return "D"

Ejecutamos la suite:

python -m pytest

12.17 Refactor: parametrizar las pruebas

Las pruebas tienen la misma forma. Podemos expresarlas como ejemplos parametrizados.

Archivo a modificar: tests/test_calificaciones.py

import pytest

from academico.calificaciones import obtener_calificacion


@pytest.mark.parametrize(
    "puntaje, esperado",
    [
        (95, "A"),
        (85, "B"),
        (75, "C"),
        (60, "D"),
    ],
)
def test_obtener_calificacion_segun_puntaje(puntaje, esperado):
    assert obtener_calificacion(puntaje) == esperado

Ejecutamos python -m pytest. Si todo pasa, el refactor fue seguro.

12.18 Agregar límites importantes

Los límites son importantes en reglas por rangos. Podemos agregar ejemplos exactos:

Archivo a modificar: tests/test_calificaciones.py

import pytest

from academico.calificaciones import obtener_calificacion


@pytest.mark.parametrize(
    "puntaje, esperado",
    [
        (90, "A"),
        (80, "B"),
        (70, "C"),
        (69, "D"),
    ],
)
def test_obtener_calificacion_en_limites_de_rango(puntaje, esperado):
    assert obtener_calificacion(puntaje) == esperado

Estos casos ayudan a detectar errores como usar > donde correspondía >=.

12.19 Errores frecuentes

  • Agregar todos los rangos de una vez: dificulta ver qué prueba guió cada cambio.
  • Olvidar los límites: los errores suelen aparecer en 90, 80, 70 y 69.
  • Generalizar demasiado temprano: puede crear una estructura más compleja de lo necesario.
  • Probar implementación interna: alcanza con verificar la calificación devuelta.
  • No refactorizar las pruebas: muchos ejemplos similares pueden expresarse mejor con parametrización.

12.20 Ejercicio propuesto

Agrega una nueva calificación "E" para puntajes menores que 50. Hazlo con TDD:

  • Escribe primero una prueba para obtener_calificacion(40).
  • Ejecuta python -m pytest y verifica que falle.
  • Modifica la función con el cambio mínimo.
  • Agrega un caso límite para 50.
  • Refactoriza las pruebas si quedan repetidas.

12.21 Lista de verificación

Antes de continuar, verifica lo siguiente:

  • Diseñaste la función agregando una regla por vez.
  • Cada nueva regla comenzó con una prueba.
  • Ejecutaste python -m pytest después de cada cambio relevante.
  • La función no depende de estado externo.
  • Agregaste casos de límite para los rangos.
  • Parametrizaste pruebas cuando varios ejemplos tenían la misma estructura.

12.22 Conclusión

En este tema usamos TDD para diseñar una función pura de manera incremental. Empezamos con una regla pequeña, agregamos nuevos ejemplos, generalizamos la implementación y refactorizamos las pruebas.

En el próximo tema trabajaremos con casos borde, valores inválidos y excepciones guiadas por pruebas.