7. Probar funciones puras y valores de retorno

7.1 Objetivo del tema

Una función pura es una función que devuelve siempre el mismo resultado cuando recibe los mismos argumentos y no modifica nada fuera de ella. No escribe archivos, no imprime en pantalla, no consulta una base de datos y no cambia variables globales.

Estas funciones son ideales para empezar a practicar testing porque sus pruebas suelen ser directas: preparamos una entrada, llamamos a la función y verificamos el valor de retorno.

Idea clave: probar una función pura consiste en comparar el resultado obtenido con el resultado esperado.

7.2 Crear una carpeta de práctica

Crea un proyecto nuevo para este tema:

mkdir funciones-puras-demo
cd funciones-puras-demo

En este tema usaremos unittest, por lo que no necesitamos instalar paquetes adicionales.

7.3 Crear el archivo con funciones puras

Crea un archivo llamado utilidades.py:

def calcular_precio_final(precio, descuento):
    precio_con_descuento = precio - (precio * descuento / 100)
    return round(precio_con_descuento, 2)


def normalizar_texto(texto):
    return texto.strip().lower()


def es_mayor_de_edad(edad):
    return edad >= 18


def obtener_iniciales(nombre_completo):
    partes = nombre_completo.strip().split()
    iniciales = [parte[0].upper() for parte in partes]
    return "".join(iniciales)


def duplicar_valores(valores):
    return [valor * 2 for valor in valores]


def resumir_producto(nombre, precio):
    return {
        "nombre": nombre.strip().title(),
        "precio": precio,
        "disponible": precio > 0,
    }

Todas estas funciones reciben datos, calculan algo y devuelven un resultado. No dependen de estado externo, por eso son fáciles de probar.

7.4 Crear el archivo de pruebas

Crea un archivo llamado test_utilidades.py:

import unittest

from utilidades import (
    calcular_precio_final,
    duplicar_valores,
    es_mayor_de_edad,
    normalizar_texto,
    obtener_iniciales,
    resumir_producto,
)


class TestUtilidades(unittest.TestCase):
    pass

La clase comienza vacía. En las próximas secciones agregaremos pruebas para distintos tipos de valores de retorno.

7.5 Probar un número devuelto

La función calcular_precio_final devuelve un número. Para verificarlo usamos assertEqual:

def test_calcular_precio_final_con_descuento(self):
    resultado = calcular_precio_final(1000, 10)

    self.assertEqual(resultado, 900)

La prueba expresa una regla simple: si el precio es 1000 y el descuento es 10, el resultado esperado es 900.

7.6 Probar redondeo de decimales

Cuando una función devuelve importes con decimales, conviene comprobar el resultado exacto que decidimos devolver:

def test_calcular_precio_final_redondea_a_dos_decimales(self):
    resultado = calcular_precio_final(99.99, 15)

    self.assertEqual(resultado, 84.99)

En este caso la función usa round, por eso esperamos un valor redondeado a dos decimales.

7.7 Probar cadenas devueltas

La función normalizar_texto elimina espacios al comienzo y al final, y convierte el texto a minúsculas:

def test_normalizar_texto_quita_espacios_y_convierte_a_minusculas(self):
    resultado = normalizar_texto("  Hola Python  ")

    self.assertEqual(resultado, "hola python")

Cuando probamos cadenas, conviene usar entradas que demuestren claramente qué transformación esperamos.

7.8 Probar valores booleanos

Para funciones que devuelven True o False, podemos usar assertTrue y assertFalse:

def test_edad_18_es_mayor_de_edad(self):
    resultado = es_mayor_de_edad(18)

    self.assertTrue(resultado)


def test_edad_17_no_es_mayor_de_edad(self):
    resultado = es_mayor_de_edad(17)

    self.assertFalse(resultado)

Estas pruebas también cubren el límite importante de la regla: los 18 años.

7.9 Probar una cadena construida

La función obtener_iniciales toma un nombre completo y devuelve las iniciales:

def test_obtener_iniciales_de_nombre_completo(self):
    resultado = obtener_iniciales("Ada Lovelace")

    self.assertEqual(resultado, "AL")

También podemos probar que maneja espacios de más:

def test_obtener_iniciales_ignora_espacios_externos(self):
    resultado = obtener_iniciales("  alan turing  ")

    self.assertEqual(resultado, "AT")

7.10 Probar listas devueltas

Las listas pueden compararse directamente con assertEqual. El orden también forma parte de la comparación:

def test_duplicar_valores_devuelve_lista_con_cada_valor_duplicado(self):
    resultado = duplicar_valores([1, 2, 3])

    self.assertEqual(resultado, [2, 4, 6])

Si el orden no fuera importante, necesitaríamos otra estrategia. En este caso sí esperamos una lista en el mismo orden que la entrada.

7.11 Probar lista vacía

Los casos simples también importan. Una lista vacía debería devolver otra lista vacía:

def test_duplicar_valores_con_lista_vacia_devuelve_lista_vacia(self):
    resultado = duplicar_valores([])

    self.assertEqual(resultado, [])

Este tipo de prueba ayuda a confirmar que la función no falla cuando recibe una colección sin elementos.

7.12 Probar diccionarios devueltos

La función resumir_producto devuelve un diccionario. Podemos comparar toda la estructura esperada:

def test_resumir_producto_devuelve_diccionario_normalizado(self):
    resultado = resumir_producto("  teclado  ", 50000)

    self.assertEqual(resultado, {
        "nombre": "Teclado",
        "precio": 50000,
        "disponible": True,
    })

Esta prueba verifica varias cosas a la vez porque forman parte de una misma salida: nombre normalizado, precio conservado y disponibilidad calculada.

7.13 Probar una parte del diccionario

A veces no necesitamos comparar todo el diccionario. Si queremos enfocarnos en una regla puntual, podemos verificar una clave:

def test_resumir_producto_sin_precio_no_esta_disponible(self):
    resultado = resumir_producto("mouse", 0)

    self.assertFalse(resultado["disponible"])

Esta prueba se concentra solamente en la regla de disponibilidad.

7.14 Archivo completo de pruebas

El archivo test_utilidades.py puede quedar así:

import unittest

from utilidades import (
    calcular_precio_final,
    duplicar_valores,
    es_mayor_de_edad,
    normalizar_texto,
    obtener_iniciales,
    resumir_producto,
)


class TestUtilidades(unittest.TestCase):

    def test_calcular_precio_final_con_descuento(self):
        resultado = calcular_precio_final(1000, 10)
        self.assertEqual(resultado, 900)

    def test_calcular_precio_final_redondea_a_dos_decimales(self):
        resultado = calcular_precio_final(99.99, 15)
        self.assertEqual(resultado, 84.99)

    def test_normalizar_texto_quita_espacios_y_convierte_a_minusculas(self):
        resultado = normalizar_texto("  Hola Python  ")
        self.assertEqual(resultado, "hola python")

    def test_edad_18_es_mayor_de_edad(self):
        resultado = es_mayor_de_edad(18)
        self.assertTrue(resultado)

    def test_edad_17_no_es_mayor_de_edad(self):
        resultado = es_mayor_de_edad(17)
        self.assertFalse(resultado)

    def test_obtener_iniciales_de_nombre_completo(self):
        resultado = obtener_iniciales("Ada Lovelace")
        self.assertEqual(resultado, "AL")

    def test_obtener_iniciales_ignora_espacios_externos(self):
        resultado = obtener_iniciales("  alan turing  ")
        self.assertEqual(resultado, "AT")

    def test_duplicar_valores_devuelve_lista_con_cada_valor_duplicado(self):
        resultado = duplicar_valores([1, 2, 3])
        self.assertEqual(resultado, [2, 4, 6])

    def test_duplicar_valores_con_lista_vacia_devuelve_lista_vacia(self):
        resultado = duplicar_valores([])
        self.assertEqual(resultado, [])

    def test_resumir_producto_devuelve_diccionario_normalizado(self):
        resultado = resumir_producto("  teclado  ", 50000)
        self.assertEqual(resultado, {
            "nombre": "Teclado",
            "precio": 50000,
            "disponible": True,
        })

    def test_resumir_producto_sin_precio_no_esta_disponible(self):
        resultado = resumir_producto("mouse", 0)
        self.assertFalse(resultado["disponible"])


if __name__ == "__main__":
    unittest.main()

7.15 Ejecutar las pruebas

Desde la carpeta del proyecto, ejecuta:

python -m unittest -v

La salida esperada será similar a:

test_calcular_precio_final_con_descuento ... ok
test_calcular_precio_final_redondea_a_dos_decimales ... ok
test_duplicar_valores_con_lista_vacia_devuelve_lista_vacia ... ok
test_duplicar_valores_devuelve_lista_con_cada_valor_duplicado ... ok
test_edad_17_no_es_mayor_de_edad ... ok
test_edad_18_es_mayor_de_edad ... ok
test_normalizar_texto_quita_espacios_y_convierte_a_minusculas ... ok
test_obtener_iniciales_de_nombre_completo ... ok
test_obtener_iniciales_ignora_espacios_externos ... ok
test_resumir_producto_devuelve_diccionario_normalizado ... ok
test_resumir_producto_sin_precio_no_esta_disponible ... ok

----------------------------------------------------------------------
Ran 11 tests in 0.001s

OK

7.16 Probar una regla por vez

Una prueba debe tener una intención clara. Si una función devuelve varios datos, podemos comparar toda la salida cuando queremos validar el contrato completo, o revisar una clave específica cuando queremos enfocarnos en una regla.

Objetivo Ejemplo
Validar toda la salida self.assertEqual(resultado, diccionario_esperado)
Validar una regla puntual self.assertFalse(resultado["disponible"])
Validar un valor booleano self.assertTrue(resultado)
Validar una lista ordenada self.assertEqual(resultado, [2, 4, 6])

7.17 Casos normales, límites y vacíos

Al probar funciones puras conviene elegir casos que representen comportamientos importantes:

  • Caso normal: una entrada típica, como una lista con varios números.
  • Caso límite: un valor justo en el borde de la regla, como edad 18.
  • Caso vacío: una cadena con espacios o una lista sin elementos.
  • Caso con formato: datos con mayúsculas, minúsculas o espacios extra.

No hace falta probar todas las combinaciones posibles. El objetivo es cubrir las reglas que realmente pueden romperse.

7.18 Errores frecuentes

  • Probar demasiadas reglas en una sola prueba: si falla, cuesta identificar la causa.
  • Usar entradas que no demuestran nada: por ejemplo normalizar un texto que ya está normalizado.
  • Olvidar casos límite: muchas reglas fallan en los bordes.
  • Comparar listas sin pensar en el orden: assertEqual exige mismos valores en el mismo orden.
  • Confundir impresión con retorno: una función que imprime no es lo mismo que una función que devuelve un valor.

7.19 Comandos usados en este tema

mkdir funciones-puras-demo
cd funciones-puras-demo
python -m unittest
python -m unittest -v
python -m unittest test_utilidades.TestUtilidades.test_edad_18_es_mayor_de_edad

7.20 Qué debes recordar de este tema

  • Las funciones puras son fáciles de probar porque dependen solo de sus argumentos.
  • La estructura básica es entrada, ejecución y comparación del resultado.
  • assertEqual sirve para números, cadenas, listas y diccionarios.
  • assertTrue y assertFalse son claros para resultados booleanos.
  • Conviene probar casos normales, límites y entradas vacías.
  • Una prueba debe expresar una intención concreta.

7.21 Conclusión

En este tema probamos funciones puras y valores de retorno. Vimos cómo verificar números, cadenas, booleanos, listas y diccionarios usando unittest.

Este tipo de prueba es la base de muchas suites automatizadas. En el próximo tema trabajaremos con funciones que no solo devuelven valores, sino que también pueden lanzar errores y excepciones esperadas.