Test Data Builder: проблема і рішення
Тестові об'єкти в фінтеху великі: платіж має суму, валюту, рахунки, статус, дати, метадані. Якщо кожен тест збирає такий об'єкт вручну, виходить шум, а при додаванні обов'язкового поля ламаються всі тести.
Test Data Builder створює об'єкт з валідними значеннями за замовчуванням і дає змінити лише те, що важливо для конкретного тесту.
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();
Тест відразу показує, що в ньому важливо: нульова сума і статус failed. Решта полів просто валідні. Методи повертають this, тому працює fluent-ланцюжок. Іменовані методи типу failed() виражають доменну мову краще, ніж withStatus('failed').
build() повертає копію, щоб повторний виклик не віддавав той самий змінюваний об'єкт.
На співбесіді: Builder дає валідний об'єкт за замовчуванням, а тест змінює лише поля, які мають значення для сценарію. Так тест читається краще, а нове обов'язкове поле я додаю в одному місці.
Builder з faker, детермінізм і коли Builder зайвий
Часто дефолти генерують через @faker-js/faker, щоб уникнути конфліктів унікальних полів при паралельному запуску:
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 }; }
}
Підводні камені:
- Невідтворюваність. Тест упав на конкретному імені з апострофом, а при перезапуску дані інші. Рішення: логувати згенеровані дані або робити
faker.seed() для відтворення. - Рандом у значеннях, від яких залежить перевірка. Якщо тест перевіряє суму, суму треба задати явно, а не брати з faker.
- Дефолти на рівні поля класу генеруються при створенні екземпляра, тому кожен
new UserBuilder() дає нового користувача. Якщо винести дефолт у статичну константу, всі тести отримають той самий email.
Коли Builder НЕ потрібен: об'єкт з 2-3 полями. Тоді досить функції з Partial: makeUser({ email }). У TypeScript object spread робить багато того, для чого в Java потрібен Builder. На співбесіді добре прозвучить саме це усвідомлення, а не "я всюди роблю білдери".
На співбесіді: Для унікальних полів я використовую faker, але значення, які перевіряє тест, задаю явно, і логую згенеровані дані, щоб падіння можна було відтворити. Для простих об'єктів обходжуся фабричною функцією з Partial, бо Builder там зайвий.