Проблема: клас сам створює свої залежності - і в тесті їх неможливо замінити.
class OrderService
{
public function place(Order $order): void
{
$gateway = new StripeGateway(config('services.stripe.key')); // справжній платіж у тесті?
$gateway->charge($order->total);
}
}
Впровадження залежностей - клас отримує залежності ззовні (зазвичай через конструктор):
class OrderService
{
public function __construct(private PaymentGateway $gateway) {}
public function place(Order $order): void
{
$this->gateway->charge($order->total);
}
}
Сервіс-контейнер Laravel створить OrderService і підставить реалізацію PaymentGateway, зареєстровану в провайдері. А в тесті можна підставити тестовий дублер.
Види тестових дублерів (за Джерардом Месарошем):
- dummy - об'єкт-заглушка, що лише заповнює параметр і не використовується;
- stub - повертає заздалегідь задані відповіді («платіж завжди успішний»);
- spy - запам'ятовує виклики, щоб потім перевірити, як його використовували;
- mock - заздалегідь налаштовані очікування викликів, що перевіряються автоматично;
- fake - робоча спрощена реалізація (база в пам'яті, платіжний шлюз, що зберігає транзакції в масиві).
У Laravel багато фейків уже готові:
Mail::fake();
Queue::fake();
Http::fake(['api.stripe.com/*' => Http::response(['status' => 'succeeded'])]);
Storage::fake('s3');
$this->app->instance(PaymentGateway::class, new FakePaymentGateway());
Чому fake часто кращі за mock: mock перевіряє, як код викликає залежність (порядок, аргументи), і ламається при будь-якому рефакторингу. Fake перевіряє результат («після оформлення в шлюзі є одна транзакція на 500 грн») - тести стійкіші.
Пастка надмірних моків: тест, де все замокано, перевіряє лише те, що код викликає моки так, як ви їх налаштували. Моки - для меж системи (сторонні API, пошта, час, випадковість), а не для кожного внутрішнього класу.