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.
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.
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.
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.
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.
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.
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.
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.
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")
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.
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.
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.
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.
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()
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
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]) |
Al probar funciones puras conviene elegir casos que representen comportamientos importantes:
No hace falta probar todas las combinaciones posibles. El objetivo es cubrir las reglas que realmente pueden romperse.
assertEqual exige mismos valores en el mismo orden.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
assertEqual sirve para números, cadenas, listas y diccionarios.assertTrue y assertFalse son claros para resultados booleanos.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.