Como estruturar dados de teste com factories e builders reutilizáveis
1. Por que usar factories e builders em testes?
1.1. Problemas com dados de teste inline
Dados de teste espalhados diretamente nos testes criam duplicação, fragilidade e manutenção complexa. Cada teste que cria um usuário manualmente repete a mesma estrutura, e qualquer mudança no modelo exige alterações em dezenas de arquivos.
// Problema: dados inline duplicados
test('deve criar pedido para usuário ativo', () => {
const user = { id: 1, name: 'João', email: 'joao@test.com', active: true };
// ... lógica do teste
});
test('deve permitir login de usuário ativo', () => {
const user = { id: 1, name: 'João', email: 'joao@test.com', active: true };
// ... duplicação exata
});
1.2. Benefícios de abstração
Factories e builders separam a construção do dado de seu uso, centralizando a lógica de criação e facilitando futuras alterações.
1.3. Diferença fundamental
- Factory: função simples que retorna um objeto com valores padrão e permite overrides
- Builder: objeto configurável com métodos encadeados para construção gradual e mais controle
2. Implementando uma Factory básica reutilizável
2.1. Estrutura mínima
Uma factory deve ter valores padrão sensíveis e aceitar parâmetros de override.
// Factory básica para usuário
function createUser(overrides = {}) {
const defaults = {
id: 1,
name: 'Usuário Padrão',
email: 'usuario@exemplo.com',
active: true,
role: 'user',
createdAt: new Date('2024-01-01')
};
return { ...defaults, ...overrides };
}
2.2. Exemplo prático de uso
// Usando a factory com diferentes overrides
const activeUser = createUser();
const adminUser = createUser({ role: 'admin', id: 2 });
const inactiveUser = createUser({ active: false, email: 'inativo@exemplo.com' });
2.3. Boas práticas com spread operator
// Factory com validação mínima
function createProduct(overrides = {}) {
const defaults = {
id: generateId(),
name: 'Produto Genérico',
price: 29.90,
category: 'eletrônicos',
inStock: true
};
// Garantir que overrides não quebrem invariantes
if (overrides.price !== undefined && overrides.price < 0) {
throw new Error('Preço não pode ser negativo');
}
return { ...defaults, ...overrides };
}
3. Evoluindo para o padrão Builder
3.1. Conceito de builder fluente
Builders permitem configuração gradual e legível através de métodos encadeados.
3.2. Implementação passo a passo
class UserBuilder {
constructor() {
this.data = {
id: 1,
name: 'Usuário Padrão',
email: 'usuario@exemplo.com',
active: true,
role: 'user',
addresses: []
};
}
withId(id) {
this.data.id = id;
return this;
}
withName(name) {
this.data.name = name;
return this;
}
withEmail(email) {
this.data.email = email;
return this;
}
withRole(role) {
this.data.role = role;
return this;
}
asAdmin() {
return this.withRole('admin');
}
asInactive() {
this.data.active = false;
return this;
}
withAddress(address) {
this.data.addresses.push(address);
return this;
}
build() {
return { ...this.data };
}
}
// Uso do builder
const user = new UserBuilder()
.withName('Maria Silva')
.withEmail('maria@exemplo.com')
.asAdmin()
.withAddress({ street: 'Rua A', number: 123 })
.build();
3.3. Herança de builders
class AdminBuilder extends UserBuilder {
constructor() {
super();
this.data.role = 'admin';
this.data.permissions = ['read', 'write', 'delete'];
}
withSpecialPermission(permission) {
this.data.permissions.push(permission);
return this;
}
}
const admin = new AdminBuilder()
.withName('Admin Principal')
.withSpecialPermission('manage_users')
.build();
4. Lidando com relacionamentos e dados aninhados
4.1. Factories interdependentes
function createOrder(overrides = {}) {
const defaults = {
id: 1,
userId: createUser().id,
items: [createOrderItem()],
total: 59.90,
status: 'pending',
createdAt: new Date()
};
return { ...defaults, ...overrides };
}
function createOrderItem(overrides = {}) {
const defaults = {
id: 1,
productId: createProduct().id,
quantity: 1,
price: 29.95
};
return { ...defaults, ...overrides };
}
4.2. Builders com associações
class OrderBuilder {
constructor() {
this.data = {
id: 1,
user: null,
items: [],
status: 'pending'
};
}
withUser(userBuilder) {
this.data.user = userBuilder.build();
return this;
}
withDefaultUser() {
return this.withUser(new UserBuilder());
}
withItems(itemBuilders) {
this.data.items = itemBuilders.map(builder => builder.build());
return this;
}
withDefaultItems(count = 1) {
const items = Array.from({ length: count }, () => new OrderItemBuilder());
return this.withItems(items);
}
build() {
return { ...this.data };
}
}
4.3. Estratégias para evitar cascatas
// Lazy evaluation para evitar criação desnecessária
class OrderBuilder {
constructor() {
this.data = {
id: 1,
userId: null,
items: [],
status: 'pending'
};
this._userBuilder = null;
}
withUser(builder) {
this._userBuilder = builder;
return this;
}
build() {
const result = { ...this.data };
// Só cria o usuário se necessário
if (this._userBuilder) {
result.user = this._userBuilder.build();
}
return result;
}
}
5. Reutilização entre cenários de teste
5.1. Biblioteca central de factories
// factories/index.js
export { createUser } from './userFactory';
export { createOrder } from './orderFactory';
export { createProduct } from './productFactory';
export { UserBuilder, AdminBuilder, GuestBuilder } from './builders';
5.2. Cenários pré-configurados
class UserBuilder {
static defaultAdmin() {
return new UserBuilder().withRole('admin').withName('Admin Padrão');
}
static defaultGuest() {
return new UserBuilder().withRole('guest').withName('Convidado');
}
static premiumUser() {
return new UserBuilder()
.withRole('user')
.withName('Usuário Premium')
.withAttribute('plan', 'premium');
}
}
5.3. Traits para composição de comportamentos
class UserBuilder {
withPremiumStatus() {
this.data.plan = 'premium';
this.data.features = ['unlimited_access', 'priority_support'];
return this;
}
withVerifiedEmail() {
this.data.emailVerified = true;
this.data.verificationDate = new Date();
return this;
}
withCompleteProfile() {
return this
.withPremiumStatus()
.withVerifiedEmail()
.withAddress({ street: 'Rua Principal', number: 100 });
}
}
6. Integração com frameworks de teste e dados únicos
6.1. Garantindo unicidade
let userIdCounter = 0;
function createUser(overrides = {}) {
userIdCounter++;
const defaults = {
id: userIdCounter,
email: `usuario${userIdCounter}@exemplo.com`,
name: `Usuário ${userIdCounter}`
};
return { ...defaults, ...overrides };
}
// Reset entre testes
beforeEach(() => {
userIdCounter = 0;
});
6.2. Uso de bibliotecas auxiliares
import { faker } from '@faker-js/faker';
function createUser(overrides = {}) {
const defaults = {
id: faker.number.int({ min: 1, max: 10000 }),
name: faker.person.fullName(),
email: faker.internet.email(),
phone: faker.phone.number(),
address: {
street: faker.location.streetAddress(),
city: faker.location.city(),
zipCode: faker.location.zipCode()
}
};
return { ...defaults, ...overrides };
}
6.3. Integração com Jest
describe('User Service', () => {
beforeEach(() => {
resetCounters();
jest.clearAllMocks();
});
test('deve criar usuário com dados únicos', () => {
const user1 = createUser();
const user2 = createUser();
expect(user1.id).not.toBe(user2.id);
expect(user1.email).not.toBe(user2.email);
});
});
7. Anti-padrões e armadilhas comuns
7.1. Factories excessivamente complexas
// EVITAR: factory que esconde lógica de negócio
function createComplexUser(overrides = {}) {
// Lógica de negócio escondida na factory
if (overrides.role === 'admin' && !overrides.permissions) {
overrides.permissions = getAllPermissions(); // Side effect inesperado
}
if (overrides.plan === 'premium' && overrides.active === undefined) {
overrides.active = true; // Decisão de negócio oculta
}
return { ...defaultUser, ...overrides };
}
// PREFERIR: factory simples + builders específicos
class PremiumUserBuilder extends UserBuilder {
constructor() {
super();
this.data.plan = 'premium';
this.data.active = true;
}
}
7.2. Overrides que quebram invariantes
// EVITAR: override que cria estado inválido
const invalidUser = createUser({ email: null }); // Invariante quebrada
// PREFERIR: validação na factory
function createUser(overrides = {}) {
if (overrides.email === null || overrides.email === undefined) {
throw new Error('Email é obrigatório');
}
// ...
}
7.3. Builders mutáveis causando efeitos colaterais
// PROBLEMA: builder compartilhado entre testes
const baseBuilder = new UserBuilder().withRole('user');
test('teste 1', () => {
const user = baseBuilder.withName('João').build();
// baseBuilder agora tem name = 'João'
});
test('teste 2', () => {
const user = baseBuilder.build(); // Contém 'João' do teste anterior!
});
// SOLUÇÃO: imutabilidade via clonagem
class UserBuilder {
withName(name) {
const clone = new UserBuilder();
clone.data = { ...this.data, name };
return clone;
}
}
8. Manutenção e evolução da estrutura de dados de teste
8.1. Versionamento de factories
// v1 - versão inicial
function createUser_v1(overrides = {}) {
return { id: 1, name: 'User', email: 'user@test.com', ...overrides };
}
// v2 - após mudança no modelo (adicionado campo 'phone')
function createUser_v2(overrides = {}) {
return {
id: 1,
name: 'User',
email: 'user@test.com',
phone: null,
...overrides
};
}
8.2. Testes que validam factories
describe('createUser factory', () => {
test('deve criar usuário com valores padrão', () => {
const user = createUser();
expect(user).toHaveProperty('id');
expect(user).toHaveProperty('name');
expect(user).toHaveProperty('email');
expect(user.active).toBe(true);
});
test('deve aplicar overrides corretamente', () => {
const user = createUser({ name: 'Custom', active: false });
expect(user.name).toBe('Custom');
expect(user.active).toBe(false);
});
});
8.3. Documentação interna com exemplos
/**
* Factory para criar usuários de teste.
*
* @example
* // Usuário padrão
* const user = createUser();
*
* @example
* // Usuário admin personalizado
* const admin = createUser({
* role: 'admin',
* email: 'admin@sistema.com'
* });
*
* @example
* // Usuário com dados completos via builder
* const user = new UserBuilder()
* .withName('Maria')
* .withEmail('maria@test.com')
* .asAdmin()
* .build();
*/
function createUser(overrides = {}) {
// ...
}
Referências
-
Martin Fowler - ObjectMother — Artigo clássico sobre padrões para criação de objetos de teste, incluindo factories e builders.
-
Factory Bot (Ruby) - Documentação Oficial — Referência prática sobre factories reutilizáveis, com conceitos aplicáveis em qualquer linguagem.
-
Jest - Setup and Teardown — Documentação oficial do Jest sobre configuração de ambiente de teste, incluindo reset de factories entre testes.
-
Faker.js - Documentação Oficial — Biblioteca para geração de dados falsos realistas, essencial para criar dados de teste variados.
-
Test Data Builders: An Alternative to Object Mother — Artigo técnico comparando builders com ObjectMother, com exemplos práticos de implementação.
-
Clean Code: A Handbook of Agile Software Craftsmanship — Capítulo sobre testes e construção de dados de teste, com princípios de design aplicáveis a factories.
-
Microsoft - Patterns for Test Data Builders — Guia da Microsoft sobre padrões para builders de dados de teste, com exemplos em C# traduzíveis para outras linguagens.