Popular banco com dados brasileiros: guia completo
Popular banco com dados brasileiros: guia completo
PostgreSQL, MySQL, SQLite e MongoDB. Seed estático vs dinâmico, migrações com fixture, reset entre testes, e o que fazer com CPF/CNPJ/CEP/cartão sem violar LGPD.
Por que dados BR importam (e por que faker.js não basta)
Popular banco de staging com dados realistas é um problema antigo. Faker.js, Faker (Python), Mockaroo e bibliotecas equivalentes resolvem o caso geral: nome, email, endereço, número de cartão. Pra contextos brasileiros, todas falham em pelo menos um lugar crítico:
- CPF e CNPJ válidos (mod-11). Faker.js gera 11 dígitos quaisquer; nenhum passa em validador real. Isso quebra integração com checkout, KYC, antifraude, qualquer fluxo que valide formato.
- Cartão por bandeira. Faker.js gera Visa/Master genéricos; Elo, Hipercard e Amex pedem prefixos BIN próprios. Testes que dependem de roteamento por bandeira (Cielo, Mercado Pago, Pagar.me) silenciosamente caem no fluxo errado.
- CEP por estado. CEP 22000-000 só existe no Rio. CEP 01000-000 só em SP. Faker.js gera 5+3 dígitos quaisquer; o teste de checkout BR rejeita.
- DDD válido por região. 11 é SP capital, 21 é RJ capital, 12 é Vale do Paraíba. Gerador genérico produz
(00) 99999-9999que não bate. - CNPJ alfanumérico. Em vigor desde 01/07/2026 (IN RFB 2.229). Faker.js não atualizou ainda. Seu app aceita os 2 formatos? Como você testa?
A solução cara é manter uma biblioteca interna que implementa os algoritmos. A solução barata é chamar uma API que já implementa. Este guia assume a segunda opção e usa o FakeForge BR como provedor (free tier: 50 chamadas/dia, sem cadastro), mas os padrões valem pra qualquer fonte que gere dado BR válido.
Decidir estratégia: estático vs dinâmico
Antes de escolher banco, decida o modelo:
| Modelo | Quando usar | Onde mora o seed |
|---|---|---|
| Estático commitado | Reproducibilidade absoluta. Debug de bug específico que precisa do mesmo CPF toda vez. | /tests/fixtures/*.sql |
| Estático gerado uma vez | Snapshot inicial salvo, regenerado quando schema muda. CI mais rápido (sem chamada de API). | /seeds/baseline.sql |
| Dinâmico por job | Padrão moderno. Dados frescos a cada run. Cobre mudança de regra (CNPJ alfanumérico) sem rebuild do seed. | Volátil, descartado pós-run |
| Híbrido | Dados "sticky" (10 user demos fixos) commitados; dados "volume" (10k orders pra testar paginação) gerados. | /seeds/demo.sql + run-time |
Pra staging compartilhado entre time, o híbrido é o sweet spot. 10-20 contas demo fixas (pra QA referenciar "teste com o user Carlos Silva") + dados de volume gerados sob demanda. Pra CI, dinâmico por job é o padrão sólido. Pra testes unit locais, estático em arquivo único.
PostgreSQL: seed via SQL gerado
PostgreSQL é o caso mais limpo. A API entrega CREATE TABLE IF NOT EXISTS + INSERT INTO diretamente, e psql aceita stdin:
# Reset rápido + seed de 500 customers psql "$DB_URL" -c "TRUNCATE customers, addresses, orders RESTART IDENTITY CASCADE;" curl -sS "https://fakeforge.com.br/api/generate?preset=customer&quantity=500&format=sql" \ | psql "$DB_URL" # 1.000 CNPJs em uma tabela própria curl -sS "https://fakeforge.com.br/api/generate?type=cnpj&quantity=1000&format=sql" \ | psql "$DB_URL" # 50 cartões Visa válidos curl -sS "https://fakeforge.com.br/api/generate?type=creditCardVisa&quantity=50&format=sql" \ | psql "$DB_URL"
O RESTART IDENTITY reseta sequences (IDs voltam pro 1), e CASCADE derruba FK referências em cadeia. Cuidado: derruba todos os dados das tabelas listadas. Em staging compartilhado, isso pode pisar no trabalho de outro dev.
Padrão alternativo pra staging compartilhado: trabalhar em schema dedicado. Cria CREATE SCHEMA test_seed, popula só ali, e cada teste usa SET search_path TO test_seed, public. Quando termina, DROP SCHEMA test_seed CASCADE tira sem afetar o resto.
Pra detalhes específicos de PostgreSQL (extensão pgcrypto pra UUID, transações pra rollback, COPY pra volume grande), veja o tutorial dedicado: Popular PostgreSQL com dados brasileiros.
MySQL: seed com TRUNCATE + INSERT
MySQL tem 2 diferenças importantes vs PostgreSQL no contexto de seed. Primeiro, TRUNCATE não aceita CASCADE; você precisa desativar FK checks ou ordenar manualmente. Segundo, o tipo VARCHAR com encoding latin1 pode comer caracteres acentuados (José, São Paulo) silenciosamente — use sempre utf8mb4 nas tabelas que recebem nomes/endereços.
# Garante encoding correto antes do seed mysql "$DB_URL" -e " SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; TRUNCATE orders; TRUNCATE addresses; TRUNCATE customers; SET FOREIGN_KEY_CHECKS = 1; " # Popula via SQL gerado curl -sS "https://fakeforge.com.br/api/generate?preset=customer&quantity=500&format=sql" \ | mysql --default-character-set=utf8mb4 "$DB_URL"
Tutorial completo: Popular MySQL com dados brasileiros.
SQLite: seed em arquivo único pra testes locais
SQLite é o pé sujo da maioria das suites de teste em Django, Rails e FastAPI: rápido, in-memory, descarta no final. Não tem TRUNCATE nem stored procedures, então o padrão é diferente:
# Gera SQL e aplica em DB nova curl -sS "https://fakeforge.com.br/api/generate?preset=customer&quantity=100&format=sql" \ -o seed.sql # Cada test run cria uma DB do zero rm -f test.db sqlite3 test.db < seed.sql # pytest, jest, ou seu runner aponta DATABASE_URL=sqlite:test.db DATABASE_URL="sqlite:./test.db" pytest
Atenção: SQLite trata FK constraints como advisory por default. Pra forçar checagem (e validar que seu seed respeita FKs), abra com PRAGMA foreign_keys = ON; no início do arquivo.
MongoDB: insertMany com documentos correlacionados
MongoDB não aceita SQL, então a estratégia muda. Pega JSON da API e usa o driver:
// seed-mongo.ts
import { MongoClient } from "mongodb";
async function seed() {
const client = await MongoClient.connect(process.env.MONGO_URL!);
const db = client.db("staging");
// Limpa coleções
await Promise.all([
db.collection("customers").drop().catch(() => {}),
db.collection("orders").drop().catch(() => {}),
]);
// Pega 500 customers correlacionados
const res = await fetch(
"https://fakeforge.com.br/api/generate?preset=customer&quantity=500"
);
const { data: customers } = await res.json();
// insertMany aceita o array direto
await db.collection("customers").insertMany(
customers.map((c: any) => ({
...c,
_id: undefined, // deixa o Mongo gerar ObjectId
createdAt: new Date(),
}))
);
console.log(`Inseridos ${customers.length} customers BR.`);
await client.close();
}
seed();Padrão pra correlacionar orders com os customers recém-inseridos:
// Pega os IDs gerados pelo Mongo
const customerIds = (await db.collection("customers").find({}, { projection: { _id: 1 } }).toArray())
.map(c => c._id);
// Gera 2000 orders correlacionados
const orderRes = await fetch(
"https://fakeforge.com.br/api/generate?type=cpf&quantity=2000"
);
const { data: cpfs } = await orderRes.json();
await db.collection("orders").insertMany(
cpfs.map((cpf: string, i: number) => ({
customer_id: customerIds[i % customerIds.length], // distribui pelos customers existentes
cpf,
total: Math.round(Math.random() * 50000) / 100,
status: ["pending", "paid", "shipped"][i % 3],
createdAt: new Date(),
}))
);Integrar com migrações: Prisma, Knex, Alembic, Django
Maioria dos ORMs separa migration (mudança de schema) de seed (popular dados). Mas integrar os dois ajuda a garantir que o seed reflete o schema atual:
Prisma:
// prisma/seed.ts
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
const res = await fetch(
"https://fakeforge.com.br/api/generate?preset=customer&quantity=200"
);
const { data } = await res.json();
await prisma.customer.createMany({
data: data.map((c: any) => ({
name: c.name,
cpf: c.cpf,
email: c.email,
})),
skipDuplicates: true,
});
}
main().finally(() => prisma.$disconnect());
// package.json
{ "prisma": { "seed": "tsx prisma/seed.ts" } }
// Roda com: npx prisma db seedKnex (Node):
// seeds/01_customers.ts
import { Knex } from "knex";
export async function seed(knex: Knex) {
await knex("customers").del();
const res = await fetch(
"https://fakeforge.com.br/api/generate?preset=customer&quantity=200"
);
const { data } = await res.json();
await knex("customers").insert(data);
}
// Roda com: knex seed:runAlembic (Python) + SQLAlchemy:
# scripts/seed.py
import requests
from sqlalchemy.orm import Session
from app.db import engine
from app.models import Customer
def main():
res = requests.get(
"https://fakeforge.com.br/api/generate",
params={"preset": "customer", "quantity": 200},
)
customers = res.json()["data"]
with Session(engine) as s:
s.query(Customer).delete()
s.add_all([Customer(**c) for c in customers])
s.commit()
if __name__ == "__main__":
main()Django:
# core/management/commands/seed.py
from django.core.management.base import BaseCommand
import requests
from core.models import Customer
class Command(BaseCommand):
def handle(self, *args, **opts):
res = requests.get(
"https://fakeforge.com.br/api/generate",
params={"preset": "customer", "quantity": 200},
)
Customer.objects.bulk_create([
Customer(**c) for c in res.json()["data"]
])
# Roda com: python manage.py seedReset entre testes: transação vs truncate vs container
Você populou. Agora, como garantir que cada teste começa numa base previsível? Três padrões, do mais leve pro mais pesado:
1. Transação rollback (mais rápido).
Cada teste roda dentro de uma transação que dá rollback no final. Funciona pra DBs que suportam SAVEPOINT (PostgreSQL, MySQL InnoDB, SQLite). Não funciona pra testes que precisam ver dados de outras conexões (smoke test de uma fila assíncrona, por exemplo). Implementação típica em pytest:
@pytest.fixture
def db_session():
conn = engine.connect()
trans = conn.begin()
session = Session(bind=conn)
yield session
session.close()
trans.rollback()
conn.close()2. TRUNCATE entre testes (consistente).
Antes de cada teste, trunca as tabelas relevantes e repopula. Lento se você tem muitas tabelas. Pode acelerar com RESTART IDENTITY em PostgreSQL ou desativando FK checks temporariamente em MySQL.
3. Container descartável (mais isolamento).
Sobe um container Docker novo por teste (testcontainers em qualquer linguagem). Mais lento (~1-3s por teste só pra subir o container), mas garante isolamento absoluto. Útil pra testes que mexem em extensões do banco, índices, ou setup complexo:
// vitest com testcontainers
import { PostgreSqlContainer } from "@testcontainers/postgresql";
let pg: any;
let connectionString: string;
beforeEach(async () => {
pg = await new PostgreSqlContainer().start();
connectionString = pg.getConnectionUri();
// Roda migrações + seed BR no container novo
await runMigrations(connectionString);
const res = await fetch(
"https://fakeforge.com.br/api/generate?preset=customer&quantity=50&format=sql"
);
await execSql(connectionString, await res.text());
});
afterEach(() => pg.stop());Anti-patterns que matam staging
- Copiar dump de produção pra staging.Maior risco LGPD, sanção de até 2% do faturamento por incidente. Mesmo "mascarando" CPF (XXX.XXX.XXX-09), a junção com email + endereço re-identifica. Use sempre fictícios.
- Mesma DB compartilhada entre dev local e staging.Cada dev mexe nos dados do colega. Causa "funciona na minha máquina" clássico. SQLite local + Postgres staging resolve.
- Seed gigante por hábito.100.000 customers porque "parece mais real". Lentidão de teste cresce linearmente. 200-500 customers cobre 99% dos cenários; pra paginação, gera 10.000 só nesse teste específico.
- Não versionar o seed script. Refactor da equação que decide cota muda comportamento; seed antigo testava cenário diferente. Commita o script (não os dados gerados) e roda na pipeline.
- Hard-code de CPF/CNPJ específico em testes. Quando você quiser regenerar seed, o teste quebra. Use IDs ou queries genéricas:
db.customers.first()em vez dedb.customers.find(cpf="123.456.789-09").
Próximos passos
Dependendo do seu stack, vale aprofundar:
- Pipeline CI/CD completa (GitHub Actions, pytest, Jest): Dados de teste no CI/CD
- PostgreSQL específico: Popular PostgreSQL com dados brasileiros
- MySQL específico: Popular MySQL com dados brasileiros
- E2E com Cypress mockando CEP: Mockar CEP em Cypress
- LGPD em ambiente de teste: LGPD em testes: guia prático
- Documentação da API: /docs
Os dados são fáceis. O custo real é decidir o modelo (estático vs dinâmico, dedicated schema vs shared, transação vs truncate) cedo o suficiente pra não refatorar a suite inteira em 6 meses. Decide agora; o resto é boilerplate.