Mock 外部服务与队列测试
High Contrast
Dark Mode
Light Mode
Sepia
Forest
1 min read186 words

Mock 外部服务与队列测试

测试不应该真实发邮件、调用 Stripe API 或触发队列 Worker。Laravel 的 Fake 机制让你在测试中"假装"调用了外部服务,然后用断言验证"调用是否发生"。


Mail Fake:测试邮件发送

use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeMail;
use App\Mail\TaskReminderMail;
it('sends welcome email after registration', function () {
Mail::fake();  // 拦截所有邮件,不真实发送
$response = $this->postJson('/api/register', [
'name'                  => 'New User',
'email'                 => 'new@example.com',
'password'              => 'password',
'password_confirmation' => 'password',
])->assertCreated();
// 断言邮件被发送
Mail::assertSent(WelcomeMail::class);
// 断言发给了正确的地址
Mail::assertSent(WelcomeMail::class, fn($mail) => $mail->hasTo('new@example.com'));
// 断言发送次数
Mail::assertSentCount(1);
// 断言没有发送某封邮件
Mail::assertNotSent(TaskReminderMail::class);
});

Queue Fake:测试 Job 分发

use Illuminate\Support\Facades\Queue;
use App\Jobs\SendWelcomeEmail;
use App\Jobs\SyncUserToExternalCRM;
it('dispatches welcome email job on registration', function () {
Queue::fake();  // 拦截所有 Job,不真实执行
$this->postJson('/api/register', [
'name'                  => 'Test User',
'email'                 => 'test@example.com',
'password'              => 'password',
'password_confirmation' => 'password',
])->assertCreated();
// 断言 Job 被分发
Queue::assertPushed(SendWelcomeEmail::class);
// 断言分发到了正确的队列
Queue::assertPushedOn('emails', SendWelcomeEmail::class);
// 断言携带了正确的数据
Queue::assertPushed(SendWelcomeEmail::class, function ($job) {
return $job->user->email === 'test@example.com';
});
// 断言分发次数
Queue::assertPushedTimes(SyncUserToExternalCRM::class, 1);
});
// 测试 Job 本身的逻辑(单独测试)
it('sends welcome email when job is executed', function () {
Mail::fake();
$user = User::factory()->create();
// 直接执行 Job 逻辑(不通过队列)
(new SendWelcomeEmail($user))->handle();
Mail::assertSent(WelcomeMail::class, fn($mail) => $mail->hasTo($user->email));
});

Event Fake:测试事件触发

use Illuminate\Support\Facades\Event;
use App\Events\TaskCompleted;
use App\Listeners\UpdateProjectProgress;
it('fires TaskCompleted event when task is marked done', function () {
Event::fake();  // 拦截所有事件,Listener 不执行
$user = User::factory()->create();
$task = Task::factory()->forUser($user)->create(['status' => 'pending']);
$this->actingAs($user, 'sanctum')
->patchJson("/api/v1/tasks/{$task->id}", ['status' => 'done'])
->assertOk();
Event::assertDispatched(TaskCompleted::class);
Event::assertDispatched(TaskCompleted::class, fn($event) => $event->task->id === $task->id);
// 测试没有触发的事件
Event::assertNotDispatched(\App\Events\TaskCreated::class);
});
// 只 Fake 特定事件(其他事件正常触发)
Event::fake([TaskCompleted::class]);

Notification Fake:测试通知

use Illuminate\Support\Facades\Notification;
use App\Notifications\TaskCompletedNotification;
it('sends notification when task is completed', function () {
Notification::fake();
$user = User::factory()->create();
$task = Task::factory()->forUser($user)->create();
$this->actingAs($user, 'sanctum')
->patchJson("/api/v1/tasks/{$task->id}", ['status' => 'done'])
->assertOk();
Notification::assertSentTo($user, TaskCompletedNotification::class);
// 断言通知的内容
Notification::assertSentTo(
$user,
TaskCompletedNotification::class,
fn($notification) => $notification->task->id === $task->id
);
// 断言没有收到通知
$otherUser = User::factory()->create();
Notification::assertNotSentTo($otherUser, TaskCompletedNotification::class);
});

HTTP Fake:Mock 外部 API 调用

use Illuminate\Support\Facades\Http;
it('syncs user to external CRM', function () {
// 模拟外部 API 响应
Http::fake([
'api.crm.com/users' => Http::response([
'id'      => 'crm_123',
'status'  => 'created',
], 201),
'api.analytics.com/*' => Http::response(['ok' => true], 200),
// 模拟失败
'api.payment.com/*' => Http::response(['error' => 'card declined'], 402),
]);
$user = User::factory()->create();
(new SyncUserToExternalCRM($user))->handle();
// 断言 HTTP 请求发生了
Http::assertSent(function ($request) use ($user) {
return $request->url() === 'https://api.crm.com/users'
&& $request->data()['email'] === $user->email;
});
Http::assertSentCount(1);
Http::assertNothingSent();  // 确认没有其他请求(如果需要)
});
// 模拟网络错误
Http::fake([
'api.stripe.com/*' => Http::throw(new \Exception('Connection refused')),
]);

单元测试:Mock 依赖

use Mockery;
use App\Repositories\TaskRepositoryInterface;
// 单元测试 Service 层(不访问数据库)
it('calculates task completion rate correctly', function () {
// 创建 Mock 对象
$repository = Mockery::mock(TaskRepositoryInterface::class);
// 设置 Mock 行为
$repository->shouldReceive('getByUser')
->once()
->with(1)
->andReturn(collect([
(object) ['status' => 'done'],
(object) ['status' => 'done'],
(object) ['status' => 'pending'],
(object) ['status' => 'pending'],
(object) ['status' => 'pending'],
]));
$service = new TaskStatisticsService($repository);
$rate = $service->completionRate(userId: 1);
expect($rate)->toBe(40.0);  // 2/5 = 40%
});
// Laravel 的 mock() 辅助(绑定到容器)
it('calls external payment service', function () {
$mock = $this->mock(\App\Services\StripeService::class);
$mock->shouldReceive('charge')
->once()
->with(Mockery::on(fn($amount) => $amount === 9900))
->andReturn(['id' => 'ch_test123', 'status' => 'succeeded']);
$order = Order::factory()->create(['total_cents' => 9900]);
$this->actingAs($order->user, 'sanctum')
->postJson("/api/orders/{$order->id}/pay")
->assertOk();
});

下一章缓存、性能优化与 Octane——测试覆盖了功能正确性,下一步是优化性能。Laravel 的缓存层次、路由/配置缓存,以及 Octane 带来的常驻内存模式。