¿Qué son las pruebas parametrizadas y por que debería importarme?
Joel Ibaceta
Posted on February 10, 2024
Imagina que estas absorto en la creación de un increíble proyecto, tu código es elegante, tus funciones eficientes ... pero ¡oh sorpresa!, tratando de cubrir todos los posibles escenarios desde tus pruebas ... estas empiezan a multiplicarse como conejos en primavera ...
Pero, no te animas a borrar ninguna! por que al parecer todas son igual de importantes, aunque prueban la misma función una y otra vez con diferentes párametros.
def test_login
pass
def test_successful_login():
pass
def test_login_without_data():
pass
def test_login_with_incorrect_password():
pass
def test_login_with_nonexistent_user():
pass
def test_login_with_banned_user():
pass
Entonces vuelves a ver los tests que acabas de escribir y te dices a ti mismo/a ... no puedo dejar mi código sin que las pruebas cubran tanto los escenarios positivos, como los de error y las diferentes validaciones de los mismos ... y la pregunta nace por si misma ¿Habrá una mejor forma de hacer esto? ...
El Mundo de las Pruebas Parametrizadas
A principios del nuevo milenio los programadores se hicieron la misma pregunta y nació una interesante tecnica llamada Pruebas Parametrizadas, que se resume basicamente en ejecutar pruebas utilizando conjuntos de datos variables.
Generación Automatica de parámetros
Uno de las primeras implementaciones de esta tecnica fue en una biblioteca de Haskell llamada QuickCheck ... esta introdujo la idea de propiedades generales basadas en lógica que deberían ser verdad para conjuntos de datos.
Pero antes de ponerme muy teórico y aburrido, veamos un ejemplo:
import Test.QuickCheck
-- Propiedad: La suma de una lista es mayor o igual a cada elemento de la lista
prop_suma_mayor_igual :: [Int] -> Bool
prop_suma_mayor_igual xs = sum xs >= minimum xs
Aquí prop_suma_mayor_igual
es una función valida que dada una lista de números la suma siempre sera mayor o igual al minimo de la lista.
Y ahora preparate para oir sobre la magia de QuickCheck ...
Al ejecutar la prueba esta biblioteca generara automaticamente una serie de diversos conjuntos de datos de prueba (listas de numeros en este caso) y verificara si prop_suma_mayor_igual
retorna el valor esperado en cada para cada uno de ellos, si ... todo de forma autonoma, repetitiva y aleatoria.
De esta forma no era necesario que cubras manualmente cada conjunto de parametros en un test independiente.
Claro te preguntaras ... pero yo no uso Haskell, ¿Puedes volver a explicármelo usando algún lenguaje mas amigable?
Hypothesis: Magia de la Generación Automática en Python
Para llevar la magia de QuickCheck
a Python contamos con la biblioteca Hypothesis
la cual nos permitira generar automaticamente los conjuntos de datos que se usaran para nuestra prueba.
Digamos que quiero asegurar que aunque haga cambios, mi nuevo algoritmo mágico de optimización de rutas siempre funcione igual o mejor que el de google maps y que esta condición siempre sea valida para desde rutas simples con puntos A -> B a rutas complejas con hasta 1000 puntos
Entonces empiezo importando las funciones relevantes para mi test:
from hypothesis import given, settings
from hypothesis.strategies import lists, floats, tuples
Luego creo la estrategia que definira con que reglas se van a generar las coordenadas:
coords = tuples(floats(min_value=-1000, max_value=1000), floats(min_value=-1000, max_value=1000))
Y una segunda estrategia que definirá cuan amplia será esta ruta
listas_coordenadas = lists(coords, min_size=1, max_size=1000)
Y listo, ahora escribiremos nuestro test
@given(listas_coordenadas)
@settings(max_examples=10)
def test_optimizacion_ruta(coords):
tiempo_algoritmo = optimizar_ruta(coords).tiempo_estimado
# gmaps.estimate es una funcion ficticia
# inventada con fines ilustrativos
# y de optimizacion de codigo
tiempo_google = gmaps.estimate(coords).time
assert tiempo_algoritmo <= tiempo_google
Y wuala! ... ahora automaticamente se generaran una serie de coordenadas por los que nuestra ruta debe de pasar una diferente cada vez.
El test ejecutara 10 combinaciones distintas aleatorias lo que hace de nuestro test menos estático, y nos da la confianza de que no estemos probando solo un conjunto controlado que hayamos medido antes y pueda estar sesgado.
Y lo mejor ... todo con un solo test, claro te preguntaras y que pasa si el test falla? pues Hypothesis
te dará un bonito mensaje que te dirá en que conjunto de datos especifico es que fallo el test ademas del feedback del assert.
Falsifying example: test_optimizacion_ruta(coords=[(0.0, 0.0), (0.0, 0.0)])
Pero ... claro asi como hay ventajas hay desventajas y si eres un prorgamador perspicaz te habras dado cuenta que la generacion aleatoria de parametros no siempre sera la mejor forma de que tus pruebas cubran los escenarios especificos que te gustaria.
Data Driven Testing: Cuando los Datos Mandan
Ese mismo problema tambien se plantearon los programadores en el pasado y nos brindaron otra forma muy interesante de hacer pruebas parametrizadas, inspirados por el Data Driven Testing
, una técnica que propone la ejecución de pruebas usando conjuntos de datos acompañados de los resultados esperados, los cuales incluso pueden estar fuera de los mismos tests y ser almacenados en archivos externos o bases de datos.
Abordar el problema con esta perspectiva nos permite separar la lógica de la prueba de los conjuntos de datos, lo que significa que si mañana hay un nuevo caso que probar, solo habría que añadirlo al final del archivo o crear un registro nuevo sin tener que tocar el código del test.
Tests Parametrizados con PyTest
Python nos provee una herramienta para testing llamada pytest la cual ya viene con funciones especificas para implementar pruebas parametrizadas con conjuntos de datos para ello usaremos pytest.mark.parametrize
Veamos un ejemplo, Imagina que estas escribiendo el test para una función de registro de usuario, por lo que te gustaría probar con un conjunto de datos muy especifico, que cubra los diferentes escenarios que se pudiesen presentar durante el registro.
Pero claro ... no quieres escribir un test individual para cada caso.
Entonces primero definiremos, una lista con los argumentos y el resultado esperado de la siguiente manera:
datos_registro = [
# Escenario 1: Todos los campos son válidos
("Usuario1", "Apellido1", "12345678", "123456789", "usuario1@example.com", True, "Registro exitoso"),
# Escenario 2: Nombre vacío
("", "Apellido2", "87654321", "987654321", "usuario2@example.com", False, "Nombre no puede estar vacío"),
# Escenario 3: Correo con formato incorrecto
("Usuario3", "Apellido3", "56789012", "111111111", "correo_invalido", False, "Formato de correo incorrecto"),
# Escenario 4: Teléfono con menos de 9 dígitos
("Usuario4", "Apellido4", "90123456", "12345678", "usuario4@example.com", False, "Número de teléfono inválido"),
# Escenario 5: Número de documento vacío
("Usuario5", "Apellido5", "", "123456789", "usuario5@example.com", False, "Número de documento no puede estar vacío"),
# Escenario 6: Correo vacío
("Usuario6", "Apellido6", "34567890", "987654321", "", False, "Correo no puede estar vacío"),
# Muchos otros escenarios
]
Luego escribimos nuestro test
@pytest.mark.parametrize("nombre, apellido, numero_documento, telefono, correo, resultado_esperado, mensaje_esperado", datos_registro)
def test_registro_usuario(
nombre, apellido, numero_documento, telefono, correo,
resultado_esperado, mensaje_esperado
):
# Ejecutamos la función de registro
resultado = registrar_usuario(
nombre, apellido, numero_documento, telefono, correo
)
# Validamos el resultado y el mensaje esperado
assert resultado["resultado"] == resultado_esperado
assert resultado["mensaje"] == mensaje_esperado
De esta manera mantenemos la simplicidad de un solo test, cubriendo el total de nuestros escenarios.
Y claro! este conjunto de datos puede venir de cualquier lugar, un archivo externo, una carpeta con archivos json uno por cada suite de tests, uno por test, una base de datos de prueba, etc ...
¿Qué hay de otros lenguajes?
Claramente imaginaras que esto no es propio de Haskell o Python sino que la comunidad de desarrolladores se ha encargado de brindarnos herramientas suficientes para que podamos crear pruebas parametrizadas en casi cualquiera de nuestros lenguajes favoritos.
Reflexión Final:
Ahora, pequeño saltamontes, has explorado dos técnicas poderosas para enfrentar el desafío de las pruebas repetitivas. Este viaje te ha mostrado cómo liberarte del peso visual de pruebas que se multiplican como la gripe. Al adoptar pruebas parametrizadas con elegancia, has limpiado tus suites de pruebas, haciendo que sean más mantenibles, confiables y legibles.
Tu equipo te lo agradecerá, y tus proyectos brillarán con una nueva luz de claridad ¡Optimiza tus pruebas, mantén tu código legible y sigue creando con confianza de que la confiabilidad de tu aplicación estará muy bien resguardada!
Posted on February 10, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024
November 30, 2024