Test Data Builder: the problem and the solution
Test objects in fintech are large: a payment has an amount, a currency, accounts, a status, dates, metadata. If each test assembles such an object by hand, you get noise, and when a required field is added all the tests break.
A Test Data Builder creates an object with valid default values and lets you change only what matters for a specific test.
export class PaymentBuilder {
private data: Payment = {
amount: 100,
currency: 'EUR',
status: 'pending',
from: 'UA001',
to: 'UA002',
};
withAmount(amount: number) { this.data.amount = amount; return this; }
withCurrency(c: Currency) { this.data.currency = c; return this; }
failed() { this.data.status = 'failed'; return this; }
build(): Payment {
return { ...this.data };
}
}
const payment = new PaymentBuilder().withAmount(0).failed().build();
The test immediately shows what matters in it: a zero amount and the status failed. The other fields are simply valid. The methods return this, so a fluent chain works. Named methods like failed() express the domain language better than withStatus('failed').
build() returns a copy, so that a repeated call doesn't hand out the same mutable object.
In an interview: A builder gives a valid object by default, and the test changes only the fields that matter for the scenario. That way the test reads better, and I add a new required field in one place.
A Builder with faker, determinism and when a Builder is unnecessary
Defaults are often generated through @faker-js/faker to avoid conflicts on unique fields in parallel runs:
import { faker } from '@faker-js/faker';
export class UserBuilder {
private data: NewUser = {
email: faker.internet.email(),
firstName: faker.person.firstName(),
password: 'Str0ng!Pass',
};
withEmail(email: string) { this.data.email = email; return this; }
build() { return { ...this.data }; }
}
Pitfalls:
- Non-reproducibility. A test failed on a specific name with an apostrophe, and on a rerun the data is different. The solution: log the generated data or use
faker.seed() for reproduction. - Randomness in values the check depends on. If a test checks an amount, the amount must be set explicitly, not taken from faker.
- Defaults at the class field level are generated when an instance is created, so each
new UserBuilder() gives a new user. If you move a default into a static constant, all tests get the same email.
When a Builder is NOT needed: an object with 2-3 fields. Then a function with Partial is enough: makeUser({ email }). In TypeScript object spread does much of what a Builder is needed for in Java. In an interview exactly this awareness sounds good, not "I make builders everywhere".
In an interview: For unique fields I use faker, but the values a test verifies I set explicitly, and I log the generated data so a failure can be reproduced. For simple objects I get by with a factory function with Partial, because a Builder is unnecessary there.