Фейк замість справжнього сервісу за фасадом
Фасад Laravel, як-от Mail чи Queue, сам нічого не вміє: кожен статичний виклик він передає обʼєкту з . Mail::fake() кладе на це місце , тобто MailFake, який нічого не надсилає, а лише записує, що йому передали. Код застосунку не змінюється: він так само пише Mail::to($user)->send(...), але лист опиняється в памʼяті фейка.
Після дії тест питає фейк через assert-методи: Mail::assertSent(), Notification::assertSentTo(), Queue::assertPushed(), Bus::assertDispatched(), Event::assertDispatched(), Http::assertSent(), а для дисків Storage::disk()->assertExists(). Кожен приймає замикання для перевірки вмісту: кому лист, з яким id job, які параметри пішли в API. Є й заперечення: assertNotSent(), assertNothingSent(), assertNothingPushed().
Http::fake() стоїть окремо: він підміняє не сервіс, а відповідь сервера. Відповіді описують за шаблоном URL, а Http::sequence() віддає їх по черзі, щоб перевірити повтор після 503. Якщо ж URL жодному шаблону не відповідає, запит тихо піде в справжню мережу. Http::preventStrayRequests() перетворює такий запит на з винятком, тож тест падає замість того, щоб ходити в API monobank.
Від фейк відрізняється порядком перевірки. Mail::shouldReceive('send')->once() описує очікуваний виклик до дії й ламається, щойно код перейде з send() на queue(). Фейк записує все й перевіряється після дії, як spy з теми про test doubles, тому тест привʼязаний до результату, а не до того, яким методом його досягли.
Що насправді робить Mail::fake()
- OrderTest використовує POST /orders (post()).
- POST /orders використовує Mail.
- Mail використовує mail.manager.
- MailManager реалізує mail.manager.
- MailFake реалізує mail.manager.
- OrderTest використовує MailFake (assertSent()).
Одне замовлення, сім фейків
POST /orders рахує доставку через API Нової пошти, надсилає покупцеві лист, повідомляє відділ продажів, ставить у чергу рахунок, кидає подію OrderPlaced і кладе квитанцію на диск. Кожна вкладка перевіряє одну з цих дій своїм фейком. fakeNovaPoshta() це маленький помічник з Http::fake(), як fakeGitHubEndpoints() у тестах цього сайту, а у вкладці Http такий фейк розписано повністю.
Фасад, контейнер і фейк своїми руками
-
1
рядки 16–21
Контейнер зберігає обʼєкт під іменем. У Laravel фасад Mail дивиться на mail.manager, і саме цей екземпляр підмінює fake().
-
2
рядки 23–44
MailFake нічого не надсилає: send() кладе лист у масив, а assertSent() шукає в ньому лист потрібного класу, що проходить перевірку з замикання.
-
3
рядки 46–58
Фасад передає кожен статичний виклик обʼєкту з контейнера. fake() кладе на те саме місце MailFake, і код застосунку різниці не помічає.
-
4
рядки 69–75
Без фейка лист пішов би в справжній SMTP, тут він кидає виняток.
-
5
рядки 77–81
Після Mail::fake() той самий код пише лист у фейк. Перша перевірка проходить, друга ні: на sales@example.com листа не було.
- Наведіть на пояснення, щоб підсвітити рядки.Торкніться пояснення, щоб підсвітити рядки.
Курс НБУ через Http::sequence()
Курс долара для зарплат у вакансіях сайт бере з API НБУ. Тест підставляє одну добру відповідь і далі 503 на кожен повтор через whenEmpty(), тож видно, що команда hiring:refresh-rates спершу зберігає курс, а коли банк лежить, падає й лишає попередній. Схожі фейки є й деінде: тести входу через GitHub вмикають Http::preventStrayRequests(), профілі розробників перевіряють фото на Storage::fake('public'), а реєстрація ловить листи через Notification::fake(). Queue::fake(), Bus::fake() і Event::fake() у наших тестах не трапляються: у застосунку немає власних jobs і подій, а черга в phpunit.xml працює як sync.
Де фейки обманюють
Event::fake() без списку
Без аргументів EventFake підміняє всі події, зокрема події Eloquent. Обсервери й хуки creating перестають працювати, і модель без згенерованого slug чи UUID ламає тест у неочікуваному місці.
Натомість: передавайте список: Event::fake([OrderPlaced::class]). Або обмежте фейк шматком коду через Event::fakeFor(fn () => ...).
assertSent() для листа в черзі
OrderConfirmation реалізує ShouldQueue, і MailFake кладе його в чергу. Mail::assertSent() падає, хоча лист «пішов».
Натомість: для таких mailable перевіряйте Mail::assertQueued(). Повідомлення assertSent() саме підкаже: Did you mean to use assertQueued() instead?
Http::fake() без preventStrayRequests()
Хтось додав виклик ще одного API, а в тесті його не описали. Запит тихо йде в справжню мережу: тест повільний, нестабільний і може створити справжню ТТН.
Натомість: викликайте Http::preventStrayRequests() у beforeEach() або в tests/Pest.php для feature-тестів. Неописаний URL тоді кидає StrayRequestException з адресою запиту.
Перевірка лише класу
Mail::assertSent(OrderConfirmation::class) пройде, навіть якщо лист пішов не тому покупцеві або з чужим замовленням.
Натомість: передавайте замикання й перевіряйте вміст: fn (OrderConfirmation $mail) => $mail->hasTo($user->email). Для заперечення є assertNotSent() з тим самим замиканням.
Фейк, мок чи нічого
- Сервіс-контейнер
- Обʼєкт Laravel, який створює й зберігає сервіси. Фасад Mail звертається до нього за сервісом mail.manager, а Facade::swap() міняє цей екземпляр на інший.
- Фейк Laravel
- Клас з Illuminate\Support\Testing\Fakes (MailFake, QueueFake, BusFake, EventFake, NotificationFake), що стає на місце справжнього сервісу, записує виклики й має assert-методи. Storage::fake() підставляє локальний диск у storage/framework/testing/disks, а Http::fake() перехоплює запити клієнта.
- Stray request
- HTTP-запит на адресу, для якої в тесті немає фейкової відповіді. Після Http::preventStrayRequests() він кидає StrayRequestException.
- Mock
- Дублер з очікуваннями до дії. У Laravel його дає Mail::shouldReceive() через Mockery: тест падає, якщо очікуваного виклику не було.
Питання зі співбесід
Він підміняє обʼєкт, що стоїть за фасадом Mail, на MailFake. Код застосунку викликає ті самі методи, але листи нікуди не йдуть, а записуються у фейк, і тест читає їх через Mail::assertSent(), assertQueued() чи assertNothingSent().
Queue::fake() підміняє чергу й бачить лише те, що в неї потрапило, разом з назвою черги: assertPushedOn('invoices', ...). Bus::fake() підміняє диспетчер команд і ловить кожен dispatch(), незалежно від того, піде job у чергу чи виконається одразу. Обидва приймають список класів, щоб підмінити тільки їх.
Без додаткових налаштувань запит піде в справжню мережу. Http::preventStrayRequests() змінює це: неописаний запит кидає StrayRequestException, і тест падає. Для sequence окремо: коли заготовлені відповіді закінчились, наступний запит кидає OutOfBoundsException, якщо не задано whenEmpty().
Фейк фасаду перевіряє лише, що запит до сервісу зроблено. Чи справжній сервіс прийме цей лист чи job, фейк не знає, тому серіалізацію, шаблон листа і handle() job треба тестувати окремо. А для власних залежностей, як клієнт Нової пошти, краще інтерфейс з in-memory реалізацією: він дає стан, який тест читає, і не привʼязаний до фасадів.