33. Pruebas unitarias de funciones puras

33.1 Introducción

Las funciones puras son una de las mejores puertas de entrada a las pruebas unitarias. Permiten concentrarse en entradas, salidas y resultados esperados sin depender de bases de datos, archivos, red, reloj del sistema ni estado global.

Cuando una función pura está bien diseñada, probarla suele ser directo: se le entregan datos, se ejecuta y se verifica el valor que devuelve.

33.2 Qué es una función pura

Una función pura cumple dos condiciones principales:

  • Para los mismos argumentos, siempre devuelve el mismo resultado.
  • No produce efectos secundarios observables fuera de la función.

Esto significa que no modifica variables globales, no escribe archivos, no imprime como parte de su comportamiento principal, no consulta servicios externos y no cambia objetos recibidos de forma inesperada.

33.3 Ejemplo mínimo

La siguiente función es pura porque solo depende de sus parámetros y devuelve un resultado.

def sumar(a, b):
    return a + b


def test_suma_dos_numeros():
    assert sumar(2, 3) == 5

No hace falta preparar un entorno complejo. La prueba expresa con claridad qué entrada se usa y qué salida se espera.

33.4 Por qué son fáciles de probar

Las funciones puras reducen la incertidumbre. Si una prueba falla, la causa está en la lógica de la función o en el resultado esperado de la prueba, no en un archivo que cambió, una conexión caída o una fecha distinta.

Esta característica hace que las pruebas sean rápidas, repetibles y fáciles de entender para quien lee el código por primera vez.

33.5 Entradas claras y salidas claras

Una buena prueba para una función pura debe mostrar con precisión qué datos entran y qué resultado debe salir.

def calcular_iva(importe, porcentaje):
    return importe * porcentaje / 100


def test_calcula_iva_del_veintiuno_por_ciento():
    resultado = calcular_iva(1000, 21)

    assert resultado == 210

El nombre de la prueba, los datos y la aserción cuentan la misma historia.

33.6 Probar el caso común

El primer caso que conviene probar suele ser el más representativo: aquel que describe el uso normal de la función.

def aplicar_descuento(precio, porcentaje):
    return precio - precio * porcentaje / 100


def test_aplica_descuento_a_un_precio():
    assert aplicar_descuento(2000, 10) == 1800

Este tipo de prueba confirma la intención principal de la función antes de avanzar hacia límites o situaciones menos frecuentes.

33.7 Probar casos límite

Los casos límite siguen siendo importantes aunque la función sea pura. Una comparación o una fórmula mal escrita puede fallar justo en el borde.

def clasificar_temperatura(grados):
    if grados < 0:
        return "bajo cero"
    if grados == 0:
        return "cero"
    return "sobre cero"


def test_cero_se_clasifica_como_cero():
    assert clasificar_temperatura(0) == "cero"

33.8 Probar entradas vacías

Muchas funciones reciben listas, textos o colecciones. En esos casos, una entrada vacía puede revelar errores importantes.

def promedio(numeros):
    if len(numeros) == 0:
        return 0
    return sum(numeros) / len(numeros)


def test_promedio_de_lista_vacia_es_cero():
    assert promedio([]) == 0

La prueba documenta una decisión: en este diseño, el promedio de una lista vacía se considera cero.

33.9 Probar valores negativos

Si una función acepta números, no hay que asumir que todos serán positivos. Los valores negativos pueden cambiar el sentido de un cálculo.

def diferencia_absoluta(a, b):
    diferencia = a - b
    if diferencia < 0:
        return -diferencia
    return diferencia


def test_diferencia_absoluta_con_resultado_negativo_intermedio():
    assert diferencia_absoluta(3, 8) == 5

33.10 Probar transformación de textos

Las funciones que limpian o transforman textos son buenas candidatas para pruebas unitarias, porque pequeños detalles suelen importar.

def crear_slug(titulo):
    return titulo.strip().lower().replace(" ", "-")


def test_crea_slug_en_minusculas_y_con_guiones():
    assert crear_slug(" Curso de Testing ") == "curso-de-testing"

La prueba verifica el comportamiento esperado sin analizar cada operación interna.

33.11 Probar listas sin modificar la entrada

Una función pura no debería modificar una lista recibida como argumento si su contrato indica que devuelve una nueva lista.

def obtener_pares(numeros):
    return [numero for numero in numeros if numero % 2 == 0]


def test_obtener_pares_no_modifica_la_lista_original():
    numeros = [1, 2, 3, 4]

    resultado = obtener_pares(numeros)

    assert resultado == [2, 4]
    assert numeros == [1, 2, 3, 4]

33.12 Evitar pruebas que repiten la función

En una prueba de función pura, el resultado esperado debe ser explícito cuando sea posible. Si la prueba repite el mismo cálculo, deja de aportar confianza.

def total_con_envio(total, envio):
    return total + envio


def test_total_con_envio():
    resultado = total_con_envio(1500, 300)

    assert resultado == 1800

Es mejor escribir 1800 que repetir 1500 + 300 como aserción principal.

33.13 Separar lógica pura de efectos secundarios

Muchas veces una unidad es difícil de probar porque mezcla cálculo con entrada y salida. Una estrategia útil es separar la lógica pura de la parte que interactúa con el exterior.

def calcular_total(precios):
    return sum(precios)


def mostrar_total(precios):
    total = calcular_total(precios)
    print(f"Total: {total}")

La función calcular_total es pura y fácil de probar. La función mostrar_total tiene un efecto secundario porque imprime en pantalla.

33.14 Probar la parte pura de un flujo

Cuando un flujo mezcla varias responsabilidades, conviene extraer la decisión o el cálculo a una función pura y probar esa parte por separado.

def calcular_puntaje(respuestas_correctas, total_preguntas):
    if total_preguntas == 0:
        return 0
    return respuestas_correctas * 100 / total_preguntas


def test_calcula_puntaje_porcentual():
    assert calcular_puntaje(8, 10) == 80

Así la prueba se concentra en la regla y no en cómo se cargan o muestran los datos.

33.15 Manejar errores en funciones puras

Una función pura también puede rechazar argumentos inválidos. La ausencia de efectos secundarios no impide que tenga un contrato estricto.

def dividir(a, b):
    if b == 0:
        raise ValueError("El divisor no puede ser cero")
    return a / b


def test_dividir_rechaza_divisor_cero():
    try:
        dividir(10, 0)
        assert False
    except ValueError as error:
        assert str(error) == "El divisor no puede ser cero"

33.16 Usar nombres que expresen el ejemplo

Como las funciones puras suelen ser pequeñas, el nombre de la prueba debe indicar el caso específico que se está verificando.

def es_par(numero):
    return numero % 2 == 0


def test_cuatro_es_par():
    assert es_par(4) is True


def test_cinco_no_es_par():
    assert es_par(5) is False

Estos nombres son más claros que test_es_par, porque muestran dos ejemplos concretos del comportamiento.

33.17 Mantener pruebas pequeñas

Una prueba de función pura suele necesitar pocas líneas. Si una prueba requiere mucha preparación, quizás la función no es tan simple como parece o está recibiendo datos demasiado complejos.

La claridad es más importante que la cantidad de verificaciones en una sola prueba. Es común escribir varias pruebas pequeñas en lugar de una prueba enorme.

33.18 Señales de una función fácil de probar

Una función pura normalmente tiene estas señales:

  • Recibe todos los datos necesarios por parámetros.
  • Devuelve un resultado en lugar de modificar el exterior.
  • No depende de la hora actual, archivos, red ni variables globales.
  • Su nombre expresa una responsabilidad concreta.
  • Puede probarse con valores simples.

33.19 Tabla de ejemplos

La siguiente tabla muestra funciones candidatas a pruebas unitarias simples.

Función Entrada Resultado esperado
sumar 2, 3 5
crear_slug "Curso de Testing" "curso-de-testing"
promedio [] 0
es_par 5 False

33.20 Qué debes recordar

Al probar funciones puras, recuerda estas ideas:

  • La prueba debe mostrar entradas y salidas con claridad.
  • El resultado esperado debe ser concreto.
  • Conviene probar casos normales, límites y errores esperados.
  • La función no debe depender de elementos externos para poder probarse fácilmente.
  • Separar lógica pura de efectos secundarios mejora el diseño y la prueba.

33.21 Conclusión

Las funciones puras son ideales para practicar pruebas unitarias porque tienen un contrato simple: reciben datos y devuelven un resultado. Esa simplicidad permite escribir pruebas claras, rápidas y repetibles.

Cuando una parte del programa es difícil de probar, muchas veces la solución no es escribir una prueba más complicada, sino separar la lógica pura de los efectos secundarios. Este enfoque mejora tanto las pruebas como el diseño del código.