Os Test Data Builders no Node.js ajudam a criar objetos de teste legíveis sem repetir dezenas de campos irrelevantes. Em vez de montar um pedido completo em cada teste, o builder fornece valores válidos por padrão e permite sobrescrever apenas o atributo importante para o cenário.
Esse padrão reduz ruído, protege testes contra mudanças de construtor e evita factories com centenas de combinações. Porém, defaults escondidos podem gerar testes pouco claros ou compartilhar objetos mutáveis. Builders devem produzir novas instâncias, usar valores determinísticos e refletir a linguagem do domínio.
Neste guia, você aprenderá a criar builders em JavaScript e TypeScript, combinar builders aninhados, testar validações, persistir fixtures, evitar aliasing e decidir entre Test Data Builder, Object Mother e factory simples.
O problema dos objetos complexos
Um teste de desconto talvez precise apenas da quantidade, mas o modelo exige vários campos:
const order = new Order({
id: 'order-1',
customerId: 'customer-1',
status: 'pending',
currency: 'BRL',
items: [
{
productId: 'product-1',
name: 'Teclado',
quantity: 10,
unitPrice: 10000
}
],
shippingAddress: {
street: 'Rua A',
city: 'São Paulo',
state: 'SP',
postalCode: '01000-000'
},
createdAt: new Date('2026-01-01T00:00:00Z')
});A maior parte desse código não participa da regra testada.
Object Mother
Martin Fowler descreve Object Mother como uma factory que cria objetos conhecidos para testes. Exemplo:
const order = OrderMother.pendingOrder();O padrão funciona para poucos cenários, mas tende a acumular métodos:
pendingOrderWithTenItems()
pendingOrderWithoutAddress()
paidOrderWithCoupon()
paidOrderWithExpiredCoupon()Test Data Builder
Nat Pryce propôs Test Data Builders como alternativa mais flexível. O builder mantém defaults válidos e métodos encadeáveis.
Builder em TypeScript
type OrderProps = {
id: string;
customerId: string;
status: 'pending' | 'paid' | 'cancelled';
currency: 'BRL';
items: OrderItem[];
shippingAddress: Address;
createdAt: Date;
};
export class OrderBuilder {
private props: OrderProps = {
id: 'order-1',
customerId: 'customer-1',
status: 'pending',
currency: 'BRL',
items: [new OrderItemBuilder().build()],
shippingAddress: new AddressBuilder().build(),
createdAt: new Date('2026-01-01T00:00:00Z')
};
withId(id: string) {
this.props.id = id;
return this;
}
withStatus(status: OrderProps['status']) {
this.props.status = status;
return this;
}
withItems(items: OrderItem[]) {
this.props.items = items;
return this;
}
build() {
return new Order(structuredClone(this.props));
}
}Teste legível
test('aplica desconto para dez unidades', () => {
const item = new OrderItemBuilder()
.withQuantity(10)
.withUnitPrice(10000)
.build();
const order = new OrderBuilder()
.withItems([item])
.build();
assert.equal(calculateDiscount(order), 10000);
});O teste destaca apenas os valores relevantes.
Defaults válidos
Um builder deve produzir um objeto válido sem customização:
const order = new OrderBuilder().build();Isso permite usar o builder em testes que não se importam com detalhes do pedido.
Valores determinísticos
Evite Date.now() e UUID aleatório como default. Eles tornam snapshots, logs e falhas difíceis de reproduzir.
createdAt: new Date('2026-01-01T00:00:00Z')Quando o teste exige variedade, passe explicitamente ou use uma seed.
Novas instâncias
Não compartilhe arrays ou objetos entre builds:
build() {
return new Order({
...this.props,
items: this.props.items.map(item => item.clone()),
shippingAddress: { ...this.props.shippingAddress },
createdAt: new Date(this.props.createdAt)
});
}Sem cópia, alterar um pedido pode afetar outro teste.
Builder imutável
Uma alternativa retorna um novo builder a cada método:
class CustomerBuilder {
constructor(private readonly props = defaultCustomer()) {}
withEmail(email: string) {
return new CustomerBuilder({ ...this.props, email });
}
build() {
return new Customer(structuredClone(this.props));
}
}Esse estilo elimina estado mutável, mas cria mais objetos. Em testes, clareza é mais importante que micro-otimização.
Builders aninhados
const order = new OrderBuilder()
.withCustomer(
new CustomerBuilder()
.withEmail('cliente@example.com')
.build()
)
.withShippingAddress(
new AddressBuilder()
.withState('SC')
.build()
)
.build();Cada builder conhece apenas o próprio objeto.
Recebendo builder
Para reduzir chamadas a build:
withCustomer(customer: Customer | CustomerBuilder) {
this.props.customer = customer instanceof CustomerBuilder
? customer.build()
: customer;
return this;
}Use com cuidado para não esconder tipos demais.
Métodos de domínio
Em vez de apenas setters genéricos:
paid() {
this.props.status = 'paid';
this.props.paidAt = new Date('2026-01-02T00:00:00Z');
return this;
}
withExpiredCoupon() {
this.props.coupon = new CouponBuilder()
.expired()
.build();
return this;
}Métodos orientados ao domínio deixam o teste mais expressivo.
Cenários inválidos
O builder normal deve criar estado válido. Para testar validação, ofereça uma saída raw ou método explícito:
withInvalidEmail() {
this.props.email = 'invalid';
return this;
}Não torne todos os setters permissivos em produção apenas para facilitar testes.
Builder de input
Separe entidades de DTOs:
const input = new CreateOrderInputBuilder()
.withoutCustomerId()
.build();Isso é útil para testes HTTP e validação de schemas.
Builder de resposta
Clientes de APIs podem usar builders para fixtures:
const response = new PaymentResponseBuilder()
.declined('insufficient_funds')
.build();Veja Mocks no Node.js Test Runner.
Persistência
Um builder de entidade não deve gravar no banco por padrão. Crie helper separado:
async function persistOrder(
database,
builder = new OrderBuilder()
) {
const order = builder.build();
await database.orders.insert(order.toPersistence());
return order;
}Assim, testes unitários continuam rápidos.
Testes de integração
test('lista pedido persistido', async () => {
const order = await persistOrder(
db,
new OrderBuilder().withStatus('paid')
);
const response = await app.inject({
method: 'GET',
url: `/orders/${order.id}`
});
assert.equal(response.statusCode, 200);
});Consulte Testcontainers no Node.js para dependências reais.
Builders e Faker
Faker pode gerar dados variados, mas não deve substituir defaults determinísticos. Um método explícito pode usar uma seed:
randomized(seed = 123) {
faker.seed(seed);
this.props.name = faker.person.fullName();
this.props.email = faker.internet.email();
return this;
}O próximo artigo aprofunda Faker.
Builders e snapshots
Use valores estáveis e remova campos irrelevantes. Snapshots com UUID e datas aleatórias mudam sem motivo.
TypeScript utility types
Para builders simples:
class ProductBuilder {
private props: ProductProps;
constructor(overrides: Partial<ProductProps> = {}) {
this.props = {
id: 'product-1',
name: 'Produto',
price: 1000,
active: true,
...overrides
};
}
build() {
return new Product({ ...this.props });
}
}Partial é prático, mas permite combinações que métodos de domínio poderiam impedir.
Factory function
Nem todo objeto precisa de classe:
function buildUser(
overrides: Partial<User> = {}
): User {
return {
id: 'user-1',
name: 'Usuário',
email: 'user@example.com',
role: 'customer',
...overrides
};
}Use factory para estruturas pequenas e builder quando há composição ou métodos expressivos.
Object Mother com builders
Os padrões podem ser combinados:
const OrderMother = {
paid() {
return new OrderBuilder().paid();
},
cancelled() {
return new OrderBuilder().cancelled();
}
};A Object Mother retorna builders, permitindo variação sem criar dezenas de factories.
Localização dos builders
test/
├── builders/
│ ├── order-builder.ts
│ ├── customer-builder.ts
│ └── address-builder.ts
└── unit/Não exporte builders no pacote de produção, salvo se fazem parte de uma biblioteca de testes pública.
Monorepos
Crie um pacote interno @empresa/testing apenas para builders reutilizados por vários projetos. Evite depender de internals dos pacotes de domínio.
Veja npm Workspaces no Node.js.
Evolução do modelo
Quando um campo obrigatório é adicionado, atualize o default em um lugar. Testes que não se importam continuam funcionando; testes específicos sobrescrevem o valor.
Não esconda intenção
Um teste importante deve declarar os valores relevantes. Evite:
const order = new OrderBuilder().build();
assert.equal(calculateTax(order), 1234);Se o resultado depende do estado, mostre:
const order = new OrderBuilder()
.withState('SC')
.withSubtotal(10000)
.build();Erros comuns
- Defaults aleatórios: falhas não reproduzem.
- Objetos compartilhados: testes interferem.
- Builder gigante: conhece todo o sistema.
- Persistência embutida: testes unitários ficam lentos.
- Partial para tudo: estados inválidos aparecem.
- Defaults escondem regra: teste fica misterioso.
- Builder em produção: código de teste entra no bundle.
- Uma factory por cenário: Object Mother incha.
Testando o builder
Builders simples não precisam de testes extensos, mas helpers complexos merecem uma verificação:
test('paid cria pedido pago consistente', () => {
const order = new OrderBuilder().paid().build();
assert.equal(order.status, 'paid');
assert.ok(order.paidAt instanceof Date);
});Conclusão
Os Test Data Builders no Node.js deixam testes focados no comportamento. Defaults válidos removem detalhes irrelevantes, enquanto métodos encadeáveis mostram apenas as diferenças do cenário.
Use dados determinísticos, novas instâncias e builders pequenos por domínio. Com factories simples para objetos pequenos e Object Mothers que retornam builders, a suíte permanece legível mesmo quando o modelo cresce.




