Estratégias de seed de banco de dados para testes de integração confiáveis
1. Fundamentos do Seed em Testes de Integração
Testes de integração exigem um estado de banco de dados previsível e isolado. Seeds dedicados são a base para alcançar esse objetivo, pois garantem que cada execução de teste comece com dados conhecidos, eliminando variáveis externas que causam flakiness.
A diferença principal entre seeds de desenvolvimento, staging e testes de integração está no propósito. Seeds de desenvolvimento focam em dados realistas para visualização; seeds de staging simulam produção com volumes grandes; já seeds de testes de integração priorizam conjuntos mínimos, determinísticos e rápidos de carregar.
Compartilhar seeds entre ambientes é arriscado. Seeds de desenvolvimento frequentemente contêm dados desatualizados ou inconsistentes, causando vazamento de estado entre testes. Efeitos colaterais como triggers ou constraints quebradas podem passar despercebidos, gerando testes falsamente positivos ou negativos.
// Exemplo de seed dedicado para teste
// seeds/test/user-seed.ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export async function seedTestUsers() {
// Dados mínimos e determinísticos
await prisma.user.createMany({
data: [
{ id: 1, name: 'Alice', email: 'alice@test.com', role: 'ADMIN' },
{ id: 2, name: 'Bob', email: 'bob@test.com', role: 'USER' },
],
})
}
2. Abordagens de Seed: Estado Global vs. Dados por Teste
A escolha entre seed global e por teste impacta diretamente velocidade e confiabilidade.
Seed único global no beforeAll é rápido, mas cria acoplamento entre testes. Se um teste modificar dados, os subsequentes podem falhar. O reset torna-se complexo, exigindo recriação completa do schema ou truncate seletivo.
Seed por teste no beforeEach oferece isolamento total. Cada teste recebe um estado limpo e conhecido. A desvantagem é o custo de performance, especialmente com muitas tabelas ou dados volumosos.
A estratégia híbrida resolve esse dilema: dados base globais (tabelas de referência, catálogos) são carregados uma vez, enquanto dados específicos por teste são inseridos dentro de transações com rollback.
// Estratégia híbrida com transação
beforeEach(async () => {
await prisma.$transaction(async (tx) => {
// Dados específicos do teste
await tx.order.create({
data: {
id: 100,
userId: 1,
total: 250.00,
status: 'PENDING',
},
})
})
})
afterEach(async () => {
// Rollback automático da transação
await prisma.$executeRawUnsafe('ROLLBACK')
})
3. Técnicas de Reset e Limpeza entre Testes
Truncate de tabelas com reinicialização de sequências é a abordagem mais direta. Garante IDs previsíveis e elimina todos os dados residuais.
// Reset completo entre testes
afterEach(async () => {
const tablenames = await prisma.$queryRaw<
Array<{ tablename: string }>
>`SELECT tablename FROM pg_tables WHERE schemaname='public'`
for (const { tablename } of tablenames) {
if (tablename !== '_prisma_migrations') {
await prisma.$executeRawUnsafe(
`TRUNCATE TABLE "${tablename}" CASCADE;`
)
// Reinicia sequências de IDs
await prisma.$executeRawUnsafe(
`ALTER SEQUENCE "${tablename}_id_seq" RESTART WITH 1;`
)
}
}
})
Transações aninhadas com savepoints oferecem isolamento mais rápido que truncate completo. Permitem reverter apenas alterações do teste atual, mantendo dados base intactos.
Deleção seletiva é útil quando apenas algumas tabelas são modificadas. Recriação do schema completo é a opção mais segura, mas também a mais lenta, adequada apenas para suítes pequenas.
4. Design de Seeds Modulares e Reutilizáveis
Fábricas de dados com padrão Builder permitem criar entidades complexas com valores padrão sensíveis e personalização por teste.
// Factory para entidade Order
class OrderFactory {
private data: any = {
id: 1,
userId: 1,
total: 100.00,
status: 'PENDING',
createdAt: new Date('2024-01-01'),
}
withId(id: number) {
this.data.id = id
return this
}
withUser(userId: number) {
this.data.userId = userId
return this
}
withStatus(status: string) {
this.data.status = status
return this
}
async create(prisma: PrismaClient) {
return prisma.order.create({ data: this.data })
}
}
Seeds hierárquicos respeitam dependências entre tabelas. Um seed de pedido depende de um seed de usuário existente. A ordem de execução deve ser controlada.
Versionamento de seeds garante compatibilidade com migrações. Cada versão de seed corresponde a uma migração específica, evitando dados órfãos ou inconsistentes.
5. Seeds Determinísticos vs. Aleatórios
Dados fixos com valores conhecidos são essenciais para asserções exatas. IDs, datas e valores numéricos previsíveis permitem comparações diretas nos testes.
// Seed determinístico
await prisma.product.create({
data: {
id: 10,
name: 'Produto Teste',
price: 49.90,
categoryId: 1,
createdAt: new Date('2024-06-15T10:00:00Z'),
},
})
// Teste usa valores conhecidos
expect(product.price).toBe(49.90)
expect(product.createdAt).toEqual(new Date('2024-06-15T10:00:00Z'))
Dados pseudoaleatórios com seed controlada ampliam a cobertura sem perder reprodutibilidade. Bibliotecas como Faker permitem gerar variações realistas enquanto mantêm resultados consistentes entre execuções.
Use dados determinísticos para testes de integridade referencial e regras de negócio fixas. Use dados aleatórios controlados para testes de borda, concorrência e stress.
6. Gerenciamento de Dados de Referência e Catálogos
Tabelas de domínio fixo (status, categorias, tipos) devem ser semeadas uma única vez, antes de qualquer teste. Esses dados raramente mudam e são compartilhados por toda a suíte.
Para catálogos grandes (milhares de produtos), use amostragem. Crie subconjuntos representativos para cada teste, evitando carregar todo o catálogo desnecessariamente.
// Seed de dados de referência (executado uma vez)
async function seedReferenceData(prisma: PrismaClient) {
await prisma.orderStatus.createMany({
data: [
{ id: 1, name: 'PENDING' },
{ id: 2, name: 'CONFIRMED' },
{ id: 3, name: 'SHIPPED' },
{ id: 4, name: 'DELIVERED' },
{ id: 5, name: 'CANCELLED' },
],
})
}
Sincronize seeds entre testes de integração e fixtures de testes unitários. Evite duplicação de dados e mantenha consistência entre camadas de teste.
7. Ferramentas e Padrões Práticos para Implementação
Bibliotecas especializadas facilitam o gerenciamento de seeds:
- Prisma Seed: integração nativa com
prisma/seedeprisma db seed - TypeORM Seeding:
typeorm-seedingcom factories e seeds modulares - Knex Seed:
knex-seedpara migrações e seeds versionados
Scripts de seed devem aceitar parâmetros de ambiente para flexibilidade:
// seed.ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
})
async function main() {
const environment = process.env.NODE_ENV || 'development'
if (environment === 'test') {
await seedTestData(prisma)
} else {
await seedDevelopmentData(prisma)
}
}
Integração com Testcontainers permite seeds executados após subida do banco isolado:
// Testcontainers + seed
const container = await new GenericContainer('postgres:15')
.withExposedPorts(5432)
.start()
const connectionString = `postgresql://user:pass@${container.getHost()}:${container.getMappedPort(5432)}/test`
process.env.DATABASE_URL = connectionString
// Executa migrações e seed
await execSync('npx prisma migrate deploy')
await execSync('npx prisma db seed')
Por fim, verifique a integridade pós-seed: constraints, índices e triggers devem estar ativos e consistentes. Um seed bem-sucedido não garante que o banco esteja em estado válido para testes.
Referências
- Prisma Seed Documentation — Guia oficial para configuração e execução de seeds com Prisma ORM
- TypeORM Seeding Guide — Biblioteca para criação de seeds e factories com TypeORM
- Testcontainers for Node.js — Documentação oficial para uso de containers Docker em testes de integração
- Database Testing Best Practices — Artigo de Martin Fowler sobre estratégias de teste em microsserviços
- Knex.js Seed Documentation — Referência oficial para criação de seeds com Knex.js