StackPractices
beginner Por Mathias Paulenko

Generar Datos de Test (Faker + Factories)

Cómo generar datos de test realistas y deterministas con Faker, factory-boy y generadores type-aware para suites de test confiables en Python, JavaScript y Java.

Temas: testing

Descripción General

Los datos de test hardcodeados (name = "John", email = "test@test.com") se vuelven obsoletos rápido, no exponen casos edge y no se parecen a las distribuciones de producción. Los generadores producen datos realistas, variados y deterministas: los tests se vuelven más confiables mientras el mantenimiento de fixtures se reduce.

Cuándo Usar

  • Mantienes docenas de objetos de test hardcodeados que divergen del esquema de producción
  • Los casos edge (strings vacíos, Unicode, valores muy largos) nunca se testean porque son tediosos de escribir
  • Los tests de integración necesitan una base de datos sembrada con cientos de filas realistas
  • Quieres que los tests ejerciten reglas de validación con distribuciones de entrada variadas
  • Los tests de carga necesitan grandes volúmenes de datos plausibles

Si el problema son los fixtures en sí — setup compartido, teardown, estado de la base de datos — combina esto con la configuración de fixtures de test.

Cuándo NO Usar

  • El test requiere un escenario muy específico y conocido — hardcodéalo explícitamente
  • El determinismo entre ejecuciones importa más que la variedad — siembra el generador pero mantén valores mínimos
  • El esquema de datos es trivial (2-3 campos) — un objeto literal es más claro
  • Estás testeando una librería tipo Faker en sí misma — usa entradas controladas y predecibles

Implementación Paso a Paso

Python

from faker import Faker
from dataclasses import dataclass
from typing import List
import factory
from factory import Faker as FactoryFaker

fake = Faker()
Faker.seed(12345)  # Determinístico entre ejecuciones

# Uso básico de Faker
fake.name()        # 'John Smith'
fake.email()       # 'john.smith@example.com'
fake.ipv4()        # '192.168.1.45'
fake.uuid4()       # '550e8400-e29b-41d4-a716-446655440000'

# factory-boy para objetos ORM
@dataclass
class User:
    id: int
    name: str
    email: str
    age: int
    is_active: bool

class UserFactory(factory.Factory):
    class Meta:
        model = User

    id = factory.Sequence(lambda n: n)
    name = FactoryFaker('name')
    email = FactoryFaker('email')
    age = factory.Faker('random_int', min=18, max=90)
    is_active = True

# Uso
user = UserFactory()                  # Instancia única
users = UserFactory.build_batch(100)  # 100 instancias
admin = UserFactory(name="Admin User", age=30)

# Provider propio para datos del dominio
from faker.providers import BaseProvider

class ProductProvider(BaseProvider):
    def sku(self):
        categories = ['ELEC', 'BOOK', 'HOME', 'TOY']
        return f"{self.random_element(categories)}-{self.random_int(1000, 9999)}"

fake.add_provider(ProductProvider)
fake.sku()  # 'ELEC-4521'

# Dataset determinista para property-based tests
import hypothesis.strategies as st

user_strategy = st.builds(
    User,
    id=st.integers(min_value=1),
    name=st.text(min_size=1, max_size=100),
    email=st.emails(),
    age=st.integers(min_value=0, max_value=120),
    is_active=st.booleans()
)

JavaScript

import { faker } from '@faker-js/faker';

// Semilla para determinismo
faker.seed(12345);

// Generadores básicos
faker.person.fullName();    // 'John Smith'
faker.internet.email();     // 'john.smith@example.com'
faker.number.int({ min: 18, max: 65 });  // 34

// Función factory
function createUser(overrides = {}) {
    return {
        id: faker.string.uuid(),
        name: faker.person.fullName(),
        email: faker.internet.email(),
        age: faker.number.int({ min: 18, max: 90 }),
        avatar: faker.image.avatar(),
        isActive: true,
        ...overrides
    };
}

// Generar lote
const users = Array.from({ length: 100 }, () => createUser());

// Helpers de faker específicos del dominio
const createOrder = (overrides = {}) => ({
    id: faker.string.uuid(),
    customerId: faker.string.uuid(),
    items: Array.from({ length: faker.number.int({ min: 1, max: 5 }) }, () => ({
        sku: `SKU-${faker.string.alphanumeric(6).toUpperCase()}`,
        quantity: faker.number.int({ min: 1, max: 10 }),
        price: faker.commerce.price({ min: 5, max: 500 })
    })),
    status: faker.helpers.arrayElement(['pending', 'paid', 'shipped', 'delivered']),
    createdAt: faker.date.past(),
    ...overrides
});

// Datos deterministas para snapshots
faker.seed(42);
const snapshotUser = createUser({ name: 'Snapshot User' });

Java

import net.datafaker.Faker;
import java.util.List;
import java.util.stream.IntStream;

public class TestDataGenerator {
    private static final Faker faker = new Faker();

    public static User createUser() {
        return User.builder()
            .id(faker.number().randomNumber())
            .name(faker.name().fullName())
            .email(faker.internet().emailAddress())
            .age(faker.number().numberBetween(18, 90))
            .isActive(true)
            .build();
    }

    public static List<User> createUsers(int count) {
        return IntStream.range(0, count)
            .mapToObj(i -> createUser())
            .toList();
    }

    // JUnit 5 parametrizado con datos generados
    public static Stream<Arguments> emailProvider() {
        return Stream.generate(() -> Arguments.of(faker.internet().emailAddress()))
            .limit(50);
    }
}

// Instancio para generación type-aware
import org.instancio.Instancio;
import org.instancio.Select;

User user = Instancio.of(User.class)
    .set(Select.field("role"), "admin")
    .generate(Select.field("age"), gen -> gen.ints().range(18, 90))
    .create();

List<User> users = Instancio.ofList(User.class).size(100).create();

Elegir un Enfoque

Cuatro herramientas cubren la mayoría de necesidades, y resuelven problemas distintos:

flowchart diagram: Necesitas datos<br>de test

Faker solo te da valores de campo realistas pero sin estructura. Las factories construyen objetos completos (o filas ORM) con defaults sensatos y overrides por test. Los generadores property-based como Hypothesis exploran el espacio de entrada por ti y reducen los fallos al caso mínimo — úsalos cuando verificas invariantes, no escenarios concretos. La receta de property-based testing profundiza en ese camino.

EnfoqueIdeal paraHerramientas
Literal hardcodeadoUn escenario exacto, tests snapshot/goldenNinguna — escribe el objeto
Faker con semillaValores de campo realistas, fuzzing de validaciónfaker (Py), @faker-js/faker, DataFaker
FactoryObjetos completos, filas ORM, overrides por testfactory-boy, Instancio, funciones factory
Property-basedInvariantes, descubrimiento de edge cases, shrinkinghypothesis, fast-check

Las suites maduras acaban combinando dos o tres: factories para el grafo de objetos, Faker dentro de ellas para los valores, y una semilla común para que cada ejecución vea los mismos datos.

Lo Que Funciona

  • Siembra siempre el generador aleatorio. Sin semilla, un test que falla en CI puede pasar en local porque los datos eran distintos. Pon Faker.seed() o faker.seed() en un archivo de setup global.
  • Sobrescribe campos concretos para tests de escenario: createUser({ role: 'admin' }) le gana a esperar que el generador aleatorio produzca un admin.
  • Restringe los rangos al dominio. Una edad aleatoria entre 0 y 120 produce mayormente datos inválidos; 18-90 encaja en usuarios adultos.
  • Genera los datos cerca del test. Un fixture users.json compartido diverge del esquema; la generación programática actualiza todos los tests al añadir un campo.
  • Incluye casos edge a propósito: strings vacíos, longitudes máximas, Unicode y nulos junto a los datos generados del camino feliz.

Errores Comunes

  • Datos aleatorios sin semilla — los tests fallan de forma intermitente porque un email aleatorio chocó con una constraint de unicidad, o un string aleatorio pareció SQL injection por accidente.
  • Rangos demasiado permisivos — los defaults de faker.number.int() son lo bastante anchos para violar reglas de negocio (precios negativos, nombres de 200 caracteres).
  • Mezclar datos generados y hardcodeados sin criterio — media suite usa Faker, la otra mitad usa literales, y nadie sabe cuál usar.
  • Exportar un fixture JSON una vez y nunca regenerarlo — los datos obsoletos no ejercitan las reglas de validación añadidas después.
  • Generadores que dependen entre sí — createOrder() llamando internamente a createUser() oculta el usuario y hace imposible afirmar sobre la relación.

Ver También

Código complementario: los generadores ejecutables para los tres lenguajes están en el repo complementario.

Preguntas frecuentes

¿Por qué debería usar factories en lugar de fixtures estáticos?

Las factories generan datos bajo demanda y se adaptan a cambios de esquema automáticamente. Un users.json en git se vuelve obsoleto y deja de ejercitar las nuevas reglas de validación. Guarda los fixtures estáticos solo para tests golden-path donde importan los datos exactos, como snapshots.

¿Qué datos nunca deberían aparecer en tests?

Nunca datos personales reales, credenciales de producción ni datos de pago. Los datos sintéticos deben parecer reales sin exponer a nadie — incluso las copias de producción "enmascaradas" pueden filtrar a través de campos residuales.

¿Cómo mantengo los datos de test deterministas entre CI y ejecuciones locales?

Siembra el generador globalmente: Faker.seed(12345) en conftest.py, faker.seed(12345) en globalSetup de Jest, new Faker(new Random(12345)) en Java. Nunca dependas de la hora del sistema o /dev/urandom — los fallos intermitentes suelen ser un generador sin semilla escondido en un helper.

¿Cómo genero datos con relaciones entre entidades?

Pasa los objetos relacionados explícitamente: const user = createUser(); const order = createOrder({ customerId: user.id }). No hagas que createOrder() llame a createUser() internamente — oculta el usuario y rompe las aserciones sobre la relación. En Python, factory.SubFactory(UserFactory) enlaza factories limpiamente.

¿Qué casos edge deberían incluir los datos generados?

Cubre los límites que tus validadores cuidan: strings vacíos, longitudes máximas, Unicode y emoji, nulos, cero, negativos y fechas en los límites (epoch, futuro lejano). Genera ~80% de los casos con Faker y añade la lista de edge explícitamente, o deja que una herramienta property-based los explore por ti.

¿Cómo manejo constraints de unicidad con datos generados?

Faker no garantiza unicidad. Añade un contador (f"user{n}@example.com"), usa factory.Sequence en factory-boy, o prefiere uuid(), que es único por diseño. No confíes en la suerte: las colisiones se hacen más probables al crecer el lote (paradoja del cumpleaños).

¿Cómo genero datos para tests de integración de base de datos?

Usa factory-boy con tu ORM: class UserFactory(factory.django.DjangoModelFactory) con Meta: model = User, luego UserFactory.create() o create_batch(1000). Envuelve los tests en una transacción con rollback para que los datos no se acumulen entre casos.

¿Puede Faker generar datos localizados para tests de i18n?

Sí — Faker('es_ES'), Faker('ja_JP') producen nombres, direcciones y teléfonos con formato local correcto. Combina con casos Unicode explícitos (CJK, RTL, emoji) y chequeos de bytes vs caracteres: un carácter CJK puede ocupar 3 bytes en UTF-8.

¿Cómo limpio los datos de test generados después de los tests?

Haz rollback de una transacción por test, o trunca un esquema de test dedicado entre suites. Para archivos, usa directorios tempfile que el SO limpia solo. Nunca apuntes nada de esto a producción.