QA Interview Hub · База знань · Тести

Патерни

Builder і Test Data Builder

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 там зайвий.