16. Generación controlada de datos de prueba con Python

16.1 Objetivo del tema

En el tema anterior usamos datos desde listas, diccionarios, CSV y JSON. Ahora veremos cómo generar datos de prueba con Python de forma controlada.

Generar datos es útil cuando necesitamos muchos casos parecidos, objetos con valores por defecto o variaciones específicas sin escribir archivos grandes.

Objetivo práctico: crear factories y helpers para generar datos de prueba repetibles, claros y fáciles de modificar.

16.2 Qué significa generación controlada

Generar datos no significa crear valores al azar sin control. En automatización, los datos deben ser repetibles. Si una prueba falla, debemos poder reproducir la misma situación.

Una buena generación de datos debe ser:

  • Predecible: el mismo caso debe poder repetirse.
  • Legible: debe entenderse qué dato se está creando.
  • Flexible: debe permitir cambiar campos específicos.
  • Reutilizable: debe servir en varias pruebas sin copiar código.

16.3 Crear una factory simple

Una factory es una función que crea datos de prueba con valores por defecto. Crea tests/helpers/usuarios_factory.py:

def crear_usuario(nombre="Ana", email="ana@example.com", activo=True):
    return {
        "nombre": nombre,
        "email": email,
        "activo": activo,
    }

Esta función permite crear usuarios válidos sin repetir el diccionario en cada prueba.

16.4 Usar la factory en una prueba

Crea tests/test_usuarios_factory.py:

from app.usuarios import obtener_email, usuario_esta_activo
from tests.helpers.usuarios_factory import crear_usuario


def test_usuario_esta_activo_con_usuario_generado_devuelve_true():
    usuario = crear_usuario()

    assert usuario_esta_activo(usuario) is True


def test_obtener_email_con_usuario_generado_devuelve_email_normalizado():
    usuario = crear_usuario(email=" ANA@EXAMPLE.COM ")

    assert obtener_email(usuario) == "ana@example.com"

La prueba pide solo el cambio que necesita. El resto de los valores queda con defaults.

16.5 Ejecutar la prueba

Ejecuta:

python -m pytest tests/test_usuarios_factory.py

La factory no se ejecuta sola. Se usa desde las pruebas o desde fixtures.

16.6 Usar overrides con diccionarios

Otra forma flexible es aceptar campos variables con **overrides:

def crear_usuario(**overrides):
    usuario = {
        "nombre": "Ana",
        "email": "ana@example.com",
        "activo": True,
    }
    usuario.update(overrides)
    return usuario

Ahora podemos escribir:

usuario = crear_usuario(activo=False)

Esto crea un usuario igual al default, pero con activo en False.

16.7 Factory para productos

Crea tests/helpers/productos_factory.py:

def crear_producto(**overrides):
    producto = {
        "nombre": "Producto de prueba",
        "precio": 100,
        "cantidad": 1,
        "descuento": 0,
    }
    producto.update(overrides)
    return producto

Esta factory sirve para pruebas de carrito, descuentos o precios.

16.8 Generar listas de productos

Podemos crear una función para generar varios productos:

def crear_productos(cantidad):
    return [
        crear_producto(nombre=f"Producto {indice}", precio=100 + indice)
        for indice in range(1, cantidad + 1)
    ]

Uso en una prueba:

from tests.helpers.productos_factory import crear_productos


def test_crear_productos_devuelve_la_cantidad_solicitada():
    productos = crear_productos(3)

    assert len(productos) == 3

16.9 Generar datos para el carrito

Podemos usar la factory para probar el total del carrito:

from app.carrito import calcular_total
from tests.helpers.productos_factory import crear_producto


def test_calcular_total_con_productos_generados_devuelve_suma_total():
    productos = [
        crear_producto(precio=100, cantidad=2),
        crear_producto(precio=50, cantidad=3),
    ]

    assert calcular_total(productos) == 350

Los datos siguen siendo claros, pero evitamos repetir todos los campos del producto.

16.10 Factories y fixtures

Una fixture puede usar una factory:

import pytest

from tests.helpers.productos_factory import crear_producto


@pytest.fixture
def productos_carrito():
    return [
        crear_producto(precio=100, cantidad=2),
        crear_producto(precio=50, cantidad=3),
    ]

La fixture define un estado común y la factory simplifica la construcción de cada dato.

16.11 Datos aleatorios controlados

Algunas veces queremos variar datos, pero sin perder repetibilidad. Para eso podemos usar una semilla.

import random


def crear_codigo_numerico(seed=1234):
    generador = random.Random(seed)
    return generador.randint(1000, 9999)

Al usar la misma semilla, obtenemos el mismo resultado cada vez.

16.12 Probar generación con semilla

Crea tests/test_datos_aleatorios.py:

from tests.helpers.usuarios_factory import crear_codigo_numerico


def test_crear_codigo_numerico_con_misma_semilla_devuelve_mismo_codigo():
    codigo_1 = crear_codigo_numerico(seed=1234)
    codigo_2 = crear_codigo_numerico(seed=1234)

    assert codigo_1 == codigo_2

La prueba no depende de azar real. El dato parece variable, pero es determinístico.

16.13 Evitar aleatoriedad sin control

No conviene escribir pruebas que dependan de valores aleatorios sin semilla:

import random


def test_ejemplo_fragil():
    valor = random.randint(1, 10)

    assert valor > 5

Esta prueba puede pasar o fallar sin que el código haya cambiado. Eso la convierte en una prueba poco confiable.

16.14 Generar correos únicos pero predecibles

Podemos generar correos a partir de un identificador:

def crear_email(indice=1):
    return f"usuario{indice}@example.com"

Uso:

def test_crear_email_con_indice_devuelve_email_predecible():
    assert crear_email(3) == "usuario3@example.com"

Esto es mejor que usar correos completamente aleatorios cuando solo necesitamos valores distintos.

16.15 Generar casos para parametrización

Una función puede generar casos para pytest.mark.parametrize:

def crear_casos_descuentos():
    return [
        (crear_producto(precio=100, descuento=0), 100),
        (crear_producto(precio=100, descuento=10), 90),
        (crear_producto(precio=100, descuento=100), 0),
    ]

Uso:

@pytest.mark.parametrize("producto, esperado", crear_casos_descuentos())
def test_aplicar_descuento_con_productos_generados(producto, esperado):
    assert aplicar_descuento(producto) == esperado

16.16 Mantener factories simples

Una factory de prueba debe ayudar, no ocultar demasiado. Si una factory tiene mucha lógica, la prueba puede volverse difícil de entender.

Buenas prácticas:

  • Valores por defecto simples.
  • Overrides explícitos.
  • Nombres de funciones claros.
  • Sin lógica de negocio compleja dentro de la factory.
  • Datos predecibles.

16.17 Ubicar factories en tests/helpers

Las factories son herramientas de prueba. Por eso conviene ubicarlas en tests/helpers:

tests/
|-- helpers/
|   |-- usuarios_factory.py
|   `-- productos_factory.py
|-- test_usuarios_factory.py
`-- test_carrito.py

No deberían mezclarse con el código de aplicación dentro de app.

16.18 Documentar datos generados

Si una factory se usa mucho, conviene documentar brevemente qué valores por defecto produce.

def crear_producto(**overrides):
    """Crea un producto válido para pruebas con precio 100 y cantidad 1."""
    producto = {
        "nombre": "Producto de prueba",
        "precio": 100,
        "cantidad": 1,
        "descuento": 0,
    }
    producto.update(overrides)
    return producto

Usa comentarios o docstrings solo cuando aporten claridad real.

16.19 Problemas frecuentes

  • La prueba falla de forma intermitente: revisa si hay aleatoriedad sin semilla.
  • La factory oculta demasiada información: usa overrides visibles en la prueba.
  • Hay datos duplicados: extrae una factory o fixture reutilizable.
  • La factory tiene lógica compleja: simplifícala o divide responsabilidades.
  • Los datos generados no son claros: usa nombres y valores de ejemplo más explícitos.

16.20 Ejercicio práctico

Crea tests/helpers/cupones_factory.py con funciones para generar cupones:

  • crear_cupon_valido
  • crear_cupon_invalido
  • crear_cupon_con_espacios

Luego crea pruebas para validar_cupon usando esas funciones.

16.21 Solución propuesta

Archivo tests/helpers/cupones_factory.py:

def crear_cupon_valido():
    return "DESC10"


def crear_cupon_invalido():
    return "DESC20"


def crear_cupon_con_espacios():
    return "  DESC10  "

Archivo tests/test_cupones_factory.py:

from app.cupones import validar_cupon
from tests.helpers.cupones_factory import (
    crear_cupon_con_espacios,
    crear_cupon_invalido,
    crear_cupon_valido,
)


def test_validar_cupon_con_cupon_valido_devuelve_true():
    assert validar_cupon(crear_cupon_valido()) is True


def test_validar_cupon_con_cupon_invalido_devuelve_false():
    assert validar_cupon(crear_cupon_invalido()) is False


def test_validar_cupon_con_espacios_devuelve_true():
    assert validar_cupon(crear_cupon_con_espacios()) is True

Ejecuta:

python -m pytest tests/test_cupones_factory.py

16.22 Lista de verificación

Antes de continuar con el próximo tema, verifica lo siguiente:

  • Sabes crear factories simples para datos de prueba.
  • Usas valores por defecto claros.
  • Usas overrides para modificar campos específicos.
  • Evitas aleatoriedad sin control.
  • Usas semillas cuando necesitas datos pseudoaleatorios.
  • Ubicas factories en tests/helpers.
  • La suite se ejecuta correctamente con python -m pytest.

16.23 Conclusión

En este tema generamos datos de prueba con Python usando factories, helpers, overrides y aleatoriedad controlada. Esta técnica permite crear datos flexibles sin depender siempre de archivos externos.

En el próximo tema automatizaremos pruebas con archivos temporales y directorios de trabajo, una habilidad útil para validar código que lee o escribe archivos.