Laravel предоставляет встроенные "fakes" для подмены реальных сервисов в тестах. Fakes перехватывают вызовы и позволяют делать assertions без реального выполнения.
Bus::fake (Job Dispatching)
<?php
use App\Jobs\ProcessOrder;
use App\Jobs\SendInvoice;
use App\Models\Order;
use Illuminate\Support\Facades\Bus;
public function test_order_dispatches_job(): void
{
Bus::fake();
$order = Order::factory()->create();
$this->postJson("/api/orders/{$order->id}/process");
// Assert job was dispatched
Bus::assertDispatched(ProcessOrder::class);
// Assert job was dispatched with specific data
Bus::assertDispatched(ProcessOrder::class, function (ProcessOrder $job) use ($order) {
return $job->order->id === $order->id;
});
// Assert dispatched count
Bus::assertDispatched(ProcessOrder::class, 1); // Exactly once
// Assert NOT dispatched
Bus::assertNotDispatched(SendInvoice::class);
// Assert nothing dispatched
// Bus::assertNothingDispatched();
}
Fake только определённые jobs
<?php
// Only fake specific jobs (others execute normally)
Bus::fake([ProcessOrder::class]);
// Everything except specified jobs are faked
Bus::fake()->except([ProcessOrder::class]);
Job Chains и Batches
<?php
use Illuminate\Support\Facades\Bus;
public function test_job_chain(): void
{
Bus::fake();
$this->postJson('/api/import', ['file' => 'data.csv']);
// Assert chain was dispatched
Bus::assertChained([
ValidateImport::class,
ProcessImport::class,
NotifyCompletion::class,
]);
}
public function test_job_batch(): void
{
Bus::fake();
$this->postJson('/api/users/send-newsletter');
Bus::assertBatched(function (\Illuminate\Bus\PendingBatch $batch) {
return $batch->name === 'newsletter'
&& $batch->jobs->count() === 100;
});
}
Event::fake
<?php
use App\Events\OrderPlaced;
use App\Events\PaymentProcessed;
use Illuminate\Support\Facades\Event;
public function test_order_fires_event(): void
{
Event::fake();
$order = Order::factory()->create();
$this->postJson("/api/orders/{$order->id}/place");
// Assert event was dispatched
Event::assertDispatched(OrderPlaced::class);
// Assert with callback
Event::assertDispatched(OrderPlaced::class, function (OrderPlaced $event) use ($order) {
return $event->order->id === $order->id;
});
// Assert count
Event::assertDispatched(OrderPlaced::class, 1);
// Assert NOT dispatched
Event::assertNotDispatched(PaymentProcessed::class);
// Assert listener is listening
Event::assertListening(
OrderPlaced::class,
SendOrderConfirmation::class
);
}
Fake определённых событий
<?php
// Only fake specific events
Event::fake([OrderPlaced::class]);
// Other events (like model events) fire normally!
// Fake all except specific events
Event::fake()->except([
\Illuminate\Auth\Events\Registered::class,
]);
Подвох на экзамене: Event::fake() без аргументов подавляет ВСЕ события, включая model events (creating, created, updating и т.д.). Это может привести к тому, что observers и model events не сработают. Для фейкинга только конкретных событий передайте массив.
fakeFor -- временный fake
<?php
// Fake events only for specific closure
$order = Event::fakeFor(function () {
// Events are faked inside this closure
$order = Order::factory()->create();
$order->place();
Event::assertDispatched(OrderPlaced::class);
return $order;
});
// Events are real again here
$order->update(['status' => 'shipped']); // This fires real events
Notification::fake
<?php
use App\Models\User;
use App\Notifications\OrderShipped;
use App\Notifications\WelcomeNotification;
use Illuminate\Support\Facades\Notification;
public function test_notification_sent(): void
{
Notification::fake();
$user = User::factory()->create();
$order = Order::factory()->for($user)->create();
$this->postJson("/api/orders/{$order->id}/ship");
// Assert notification was sent to specific user
Notification::assertSentTo($user, OrderShipped::class);
// Assert with callback
Notification::assertSentTo(
$user,
OrderShipped::class,
function (OrderShipped $notification, array $channels) use ($order) {
return $notification->order->id === $order->id
&& in_array('mail', $channels);
}
);
// Assert notification was sent to multiple users
$users = User::factory(3)->create();
Notification::assertSentTo($users, WelcomeNotification::class);
// Assert NOT sent
Notification::assertNotSentTo($user, WelcomeNotification::class);
// Assert nothing was sent
// Notification::assertNothingSent();
// Assert count
Notification::assertCount(1);
}
On-Demand Notifications
<?php
public function test_on_demand_notification(): void
{
Notification::fake();
// On-demand notification (no User model)
\Illuminate\Support\Facades\Notification::route('mail', '[email protected]')
->notify(new OrderShipped($order));
Notification::assertSentOnDemand(
OrderShipped::class,
function (OrderShipped $notification, array $channels, $notifiable) {
return $notifiable->routes['mail'] === '[email protected]';
}
);
}
Queue::fake
<?php
use App\Jobs\ProcessPayment;
use App\Jobs\SendReceipt;
use Illuminate\Support\Facades\Queue;
public function test_jobs_pushed_to_queue(): void
{
Queue::fake();
$this->postJson('/api/payments', [
'amount' => 100,
'method' => 'card',
]);
// Assert job was pushed
Queue::assertPushed(ProcessPayment::class);
// Assert pushed to specific queue
Queue::assertPushedOn('payments', ProcessPayment::class);
// Assert with callback
Queue::assertPushed(ProcessPayment::class, function (ProcessPayment $job) {
return $job->amount === 100;
});
// Assert NOT pushed
Queue::assertNotPushed(SendReceipt::class);
// Assert count
Queue::assertCount(1);
// Assert nothing pushed
// Queue::assertNothingPushed();
}
Подвох на экзамена: Bus::fake() и Queue::fake() -- это разные вещи. Bus::fake() перехватывает dispatch(). Queue::fake() перехватывает Queue::push() и Queue::later(). Для стандартных dispatchable jobs используйте Bus::fake().
Storage::fake
<?php
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
public function test_file_upload(): void
{
// Create fake disk (in-memory filesystem)
Storage::fake('public');
$file = UploadedFile::fake()->image('avatar.jpg', 200, 200);
$this->postJson('/api/avatar', [
'avatar' => $file,
]);
// Assert file exists on fake disk
Storage::disk('public')->assertExists('avatars/avatar.jpg');
// Assert file does NOT exist
Storage::disk('public')->assertMissing('avatars/old.jpg');
// Assert directory exists
Storage::disk('public')->assertDirectoryExists('avatars');
// Assert file count
$files = Storage::disk('public')->files('avatars');
$this->assertCount(1, $files);
}
Storage assertions
<?php
Storage::fake('s3');
// After uploading...
Storage::disk('s3')->assertExists('path/to/file.pdf');
Storage::disk('s3')->assertMissing('path/to/nonexistent.pdf');
// Check file content
$content = Storage::disk('s3')->get('path/to/file.txt');
$this->assertEquals('expected content', $content);
// Check file size
$size = Storage::disk('s3')->size('path/to/file.pdf');
$this->assertGreaterThan(0, $size);
Mail::fake
<?php
use App\Mail\OrderConfirmation;
use App\Mail\WelcomeMail;
use Illuminate\Support\Facades\Mail;
public function test_order_email_sent(): void
{
Mail::fake();
$user = User::factory()->create();
$order = Order::factory()->for($user)->create();
$this->postJson("/api/orders/{$order->id}/confirm");
// Assert mailable was sent
Mail::assertSent(OrderConfirmation::class);
// Assert sent to specific address
Mail::assertSent(OrderConfirmation::class, function (OrderConfirmation $mail) use ($user) {
return $mail->hasTo($user->email)
&& $mail->order->id === $user->id;
});
// Assert NOT sent
Mail::assertNotSent(WelcomeMail::class);
// Assert queued (if using ShouldQueue)
Mail::assertQueued(OrderConfirmation::class);
Mail::assertNotQueued(WelcomeMail::class);
// Assert nothing sent
// Mail::assertNothingSent();
// Mail::assertNothingQueued();
// Assert count
Mail::assertSentCount(1);
}
Mail assertions на содержимое
<?php
Mail::fake();
// ... trigger mail
Mail::assertSent(OrderConfirmation::class, function (OrderConfirmation $mail) {
// Check recipients
$mail->hasTo('[email protected]');
$mail->hasCc('[email protected]');
$mail->hasBcc('[email protected]');
$mail->hasFrom('[email protected]');
$mail->hasReplyTo('[email protected]');
// Check subject
$mail->hasSubject('Order Confirmation');
// Check attachments
$mail->hasAttachment(\Illuminate\Mail\Mailables\Attachment::fromPath('/path/to/invoice.pdf'));
// Check metadata/tags
$mail->hasMetadata('order_id', '123');
$mail->hasTag('order');
return true;
});
Http::fake
<?php
use Illuminate\Support\Facades\Http;
public function test_external_api_call(): void
{
// Fake ALL HTTP requests
Http::fake();
// Fake specific URLs
Http::fake([
'api.github.com/*' => Http::response(['login' => 'octocat'], 200),
'api.stripe.com/charges' => Http::response(['id' => 'ch_123'], 201),
'*' => Http::response('Not Found', 404), // Fallback for all other URLs
]);
$this->postJson('/api/charge', ['amount' => 1000]);
// Assert request was made
Http::assertSent(function (\Illuminate\Http\Client\Request $request) {
return $request->url() === 'https://api.stripe.com/charges'
&& $request->hasHeader('Authorization')
&& $request['amount'] === 1000;
});
// Assert NOT sent to specific URL
Http::assertNotSent(function (\Illuminate\Http\Client\Request $request) {
return str_contains($request->url(), 'api.github.com');
});
// Assert exact count of requests
Http::assertSentCount(1);
// Assert nothing sent
// Http::assertNothingSent();
}
Sequence responses
<?php
Http::fake([
'api.example.com/*' => Http::sequence()
->push(['status' => 'pending'], 200) // First request
->push(['status' => 'complete'], 200) // Second request
->pushStatus(429) // Third request: rate limited
->whenEmpty(Http::response('', 500)), // All subsequent requests
]);
// First call: 200 with status=pending
// Second call: 200 with status=complete
// Third call: 429
// Fourth+ calls: 500
Prevent stray requests
<?php
// Throw exception if any un-faked request is made
Http::preventStrayRequests();
Http::fake([
'api.known.com/*' => Http::response('OK'),
]);
// This will throw an exception:
// Http::get('https://api.unknown.com/data');
Mockery
Мокирование сервисов
<?php
use App\Services\PaymentGateway;
use Mockery\MockInterface;
public function test_payment_processing(): void
{
// Create a mock
$mock = $this->mock(PaymentGateway::class, function (MockInterface $mock) {
$mock->shouldReceive('charge')
->once()
->with(100, 'USD')
->andReturn(['id' => 'txn_123', 'status' => 'success']);
});
// The mock is automatically bound in the container
$response = $this->postJson('/api/payments', [
'amount' => 100,
'currency' => 'USD',
]);
$response->assertOk()
->assertJsonPath('data.transaction_id', 'txn_123');
}
Partial Mock
<?php
use App\Services\OrderService;
public function test_order_with_partial_mock(): void
{
// Partial mock: only mock specific methods
$this->partialMock(OrderService::class, function (MockInterface $mock) {
$mock->shouldReceive('calculateTax')
->andReturn(10.00);
// All other methods work normally!
});
$response = $this->postJson('/api/orders', [
'items' => [['product_id' => 1, 'qty' => 2]],
]);
$response->assertOk();
}
Подвох на экзамене: $this->mock() заменяет ВСЕ методы сервиса. Немокированные методы возвращают null. $this->partialMock() мокирует только указанные методы, остальные работают нормально.
Spy
<?php
public function test_with_spy(): void
{
// Spy: records calls but executes real methods
$spy = $this->spy(PaymentGateway::class);
$this->postJson('/api/payments', [
'amount' => 100,
'currency' => 'USD',
]);
// Verify after the fact (spy pattern)
$spy->shouldHaveReceived('charge')
->once()
->with(100, 'USD');
$spy->shouldNotHaveReceived('refund');
}
Подвох на экзамене: Spy отличается от Mock тем, что spy записывает вызовы и позволяет проверять их ПОСЛЕ выполнения. Mock требует определения ожиданий ПЕРЕД выполнением.
Mockery expectations
<?php
$mock = \Mockery::mock(PaymentGateway::class);
// Call expectations
$mock->shouldReceive('charge')->once(); // Exactly 1 time
$mock->shouldReceive('charge')->twice(); // Exactly 2 times
$mock->shouldReceive('charge')->times(3); // Exactly 3 times
$mock->shouldReceive('charge')->atLeast()->once(); // At least 1 time
$mock->shouldReceive('charge')->atMost()->twice(); // At most 2 times
$mock->shouldReceive('charge')->never(); // Never called
// Argument matching
$mock->shouldReceive('charge')->with(100); // Exact value
$mock->shouldReceive('charge')->with(\Mockery::any()); // Any value
$mock->shouldReceive('charge')->with(\Mockery::type('int')); // Type check
$mock->shouldReceive('charge')->withArgs(fn ($amount) => $amount > 0); // Callback
// Return values
$mock->shouldReceive('charge')->andReturn('result');
$mock->shouldReceive('charge')->andReturnNull();
$mock->shouldReceive('charge')->andReturnTrue();
$mock->shouldReceive('charge')->andReturnFalse();
$mock->shouldReceive('charge')->andThrow(new \Exception('Failed'));
$mock->shouldReceive('charge')->andReturnUsing(fn ($amount) => $amount * 1.1);
// Sequential returns
$mock->shouldReceive('getStatus')
->andReturn('pending', 'processing', 'complete'); // 1st, 2nd, 3rd call
Mocking Facades directly
<?php
use Illuminate\Support\Facades\Cache;
public function test_caching(): void
{
Cache::shouldReceive('get')
->once()
->with('users')
->andReturn(collect([/* users */]));
Cache::shouldReceive('put')
->once()
->with('users', \Mockery::type('object'), \Mockery::any());
// Execute code that uses Cache...
}
Pest-стиль мокирования
<?php
use App\Services\PaymentGateway;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Bus;
test('order processing dispatches job', function () {
Bus::fake();
$order = Order::factory()->create();
$this->postJson("/api/orders/{$order->id}/process");
Bus::assertDispatched(ProcessOrder::class, fn ($job) =>
$job->order->id === $order->id
);
});
test('payment gateway is called correctly', function () {
$this->mock(PaymentGateway::class, function ($mock) {
$mock->shouldReceive('charge')
->once()
->with(100, 'USD')
->andReturn(['status' => 'success']);
});
$this->postJson('/api/charge', ['amount' => 100, 'currency' => 'USD'])
->assertOk();
});
test('events are fired on registration', function () {
Event::fake([\App\Events\UserRegistered::class]);
$this->postJson('/api/register', [
'name' => 'John',
'email' => '[email protected]',
'password' => 'SecurePass123!',
])->assertCreated();
Event::assertDispatched(\App\Events\UserRegistered::class);
});