模拟
介绍
在测试 Laravel 应用程序时,您可能希望“模拟”应用程序的某些方面,以便在给定测试期间不实际执行它们。例如,在测试调度事件的控制器时,您可能希望模拟事件监听器,以便在测试期间不实际执行它们。这使您可以仅测试控制器的 HTTP 响应,而无需担心事件监听器的执行,因为事件监听器可以在自己的测试用例中进行测试。
Laravel 提供了用于模拟事件、作业和门面的助手。这些助手主要提供了一个便利层,覆盖了 Mockery,因此您不必手动进行复杂的 Mockery 方法调用。您还可以使用 Mockery 或 PHPUnit 创建自己的模拟或间谍。
模拟对象
当模拟一个将通过 Laravel 的服务容器注入到您的应用程序中的对象时,您需要将您的模拟实例绑定到容器中作为 instance
绑定。这将指示容器使用您的模拟对象实例,而不是自行构造对象:
use Mockery;
use App\Service;
$this->instance(Service::class, Mockery::mock(Service::class, function ($mock) {
$mock->shouldReceive('process')->once();
}));
为了使这更方便,您可以使用 mock
方法,这是由 Laravel 的基础测试用例类提供的:
use App\Service;
$this->mock(Service::class, function ($mock) {
$mock->shouldReceive('process')->once();
});
同样,如果您想监视一个对象,Laravel 的基础测试用例类提供了一个 spy
方法,作为 Mockery::spy
方法的便捷包装:
use App\Service;
$this->spy(Service::class, function ($mock) {
$mock->shouldHaveReceived('process');
});
总线假对象
作为模拟的替代方案,您可以使用 Bus
门面的 fake
方法来防止作业被调度。在使用假对象时,断言是在测试代码执行后进行的:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Jobs\ShipOrder;
use Illuminate\Support\Facades\Bus;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
class ExampleTest extends TestCase
{
public function testOrderShipping()
{
Bus::fake();
// 执行订单发货...
Bus::assertDispatched(ShipOrder::class, function ($job) use ($order) {
return $job->order->id === $order->id;
});
// 断言作业未被调度...
Bus::assertNotDispatched(AnotherJob::class);
}
}
事件假对象
作为模拟的替代方案,您可以使用 Event
门面的 fake
方法来防止所有事件监听器执行。然后您可以断言事件已被调度,甚至检查它们接收到的数据。在使用假对象时,断言是在测试代码执行后进行的:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Events\OrderShipped;
use App\Events\OrderFailedToShip;
use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
class ExampleTest extends TestCase
{
/**
* 测试订单发货。
*/
public function testOrderShipping()
{
Event::fake();
// 执行订单发货...
Event::assertDispatched(OrderShipped::class, function ($e) use ($order) {
return $e->order->id === $order->id;
});
// 断言事件被调度了两次...
Event::assertDispatched(OrderShipped::class, 2);
// 断言事件未被调度...
Event::assertNotDispatched(OrderFailedToShip::class);
}
}
调用 Event::fake()
后,任何事件监听器都不会被执行。因此,如果您的测试使用依赖于事件的模型工厂,例如在模型的 creating
事件期间创建 UUID,您应该在使用工厂后调用 Event::fake()
。
模拟一部分事件
如果您只想模拟特定事件集的事件监听器,您可以将它们传递给 fake
或 fakeFor
方法:
/**
* 测试订单处理。
*/
public function testOrderProcess()
{
Event::fake([
OrderCreated::class,
]);
$order = factory(Order::class)->create();
Event::assertDispatched(OrderCreated::class);
// 其他事件正常调度...
$order->update([...]);
}
范围事件假对象
如果您只想在测试的一部分中模拟事件监听器,您可以使用 fakeFor
方法:
<?php
namespace Tests\Feature;
use App\Order;
use Tests\TestCase;
use App\Events\OrderCreated;
use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
class ExampleTest extends TestCase
{
/**
* 测试订单处理。
*/
public function testOrderProcess()
{
$order = Event::fakeFor(function () {
$order = factory(Order::class)->create();
Event::assertDispatched(OrderCreated::class);
return $order;
});
// 事件正常调度,观察者将运行...
$order->update([...]);
}
}
邮件假对象
您可以使用 Mail
门面的 fake
方法来防止邮件被发送。然后您可以断言 邮件 已发送给用户,甚至检查它们接收到的数据。在使用假对象时,断言是在测试代码执行后进行的:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Mail\OrderShipped;
use Illuminate\Support\Facades\Mail;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
class ExampleTest extends TestCase
{
public function testOrderShipping()
{
Mail::fake();
// 断言没有邮件被发送...
Mail::assertNothingSent();
// 执行订单发货...
Mail::assertSent(OrderShipped::class, function ($mail) use ($order) {
return $mail->order->id === $order->id;
});
// 断言消息已发送给给定用户...
Mail::assertSent(OrderShipped::class, function ($mail) use ($user) {
return $mail->hasTo($user->email) &&
$mail->hasCc('...') &&
$mail->hasBcc('...');
});
// 断言邮件被发送了两次...
Mail::assertSent(OrderShipped::class, 2);
// 断言邮件未被发送...
Mail::assertNotSent(AnotherMailable::class);
}
}
如果您将邮件排队在后台发送,您应该使用 assertQueued
方法而不是 assertSent
:
Mail::assertQueued(...);
Mail::assertNotQueued(...);
通知假对象
您可以使用 Notification
门面的 fake
方法来防止通知被发送。然后您可以断言 通知 已发送给用户,甚至检查它们接收到的数据。在使用假对象时,断言是在测试代码执行后进行的:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Notifications\OrderShipped;
use Illuminate\Support\Facades\Notification;
use Illuminate\Notifications\AnonymousNotifiable;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
class ExampleTest extends TestCase
{
public function testOrderShipping()
{
Notification::fake();
// 断言没有通知被发送...
Notification::assertNothingSent();
// 执行订单发货...
Notification::assertSentTo(
$user,
OrderShipped::class,
function ($notification, $channels) use ($order) {
return $notification->order->id === $order->id;
}
);
// 断言通知已发送给给定用户...
Notification::assertSentTo(
[$user], OrderShipped::class
);
// 断言通知未被发送...
Notification::assertNotSentTo(
[$user], AnotherNotification::class
);
// 断言通知通过 Notification::route() 方法发送...
Notification::assertSentTo(
new AnonymousNotifiable, OrderShipped::class
);
}
}
队列假对象
作为模拟的替代方案,您可以使用 Queue
门面的 fake
方法来防止作业被排队。然后您可以断言作业已被推送到队列,甚至检查它们接收到的数据。在使用假对象时,断言是在测试代码执行后进行的:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Jobs\ShipOrder;
use Illuminate\Support\Facades\Queue;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
class ExampleTest extends TestCase
{
public function testOrderShipping()
{
Queue::fake();
// 断言没有作业被推送...
Queue::assertNothingPushed();
// 执行订单发货...
Queue::assertPushed(ShipOrder::class, function ($job) use ($order) {
return $job->order->id === $order->id;
});
// 断言作业被推送到给定队列...
Queue::assertPushedOn('queue-name', ShipOrder::class);
// 断言作业被推送了两次...
Queue::assertPushed(ShipOrder::class, 2);
// 断言作业未被推送...
Queue::assertNotPushed(AnotherJob::class);
// 断言作业被推送了特定的链...
Queue::assertPushedWithChain(ShipOrder::class, [
AnotherJob::class,
FinalJob::class
]);
}
}
存储假对象
Storage
门面的 fake
方法允许您轻松生成一个假磁盘,结合 UploadedFile
类的文件生成工具,大大简化了文件上传的测试。例如:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
class ExampleTest extends TestCase
{
public function testAlbumUpload()
{
Storage::fake('photos');
$response = $this->json('POST', '/photos', [
UploadedFile::fake()->image('photo1.jpg'),
UploadedFile::fake()->image('photo2.jpg')
]);
// 断言一个或多个文件已存储...
Storage::disk('photos')->assertExists('photo1.jpg');
Storage::disk('photos')->assertExists(['photo1.jpg', 'photo2.jpg']);
// 断言一个或多个文件未存储...
Storage::disk('photos')->assertMissing('missing.jpg');
Storage::disk('photos')->assertMissing(['missing.jpg', 'non-existing.jpg']);
}
}
默认情况下,fake
方法将删除其临时目录中的所有文件。如果您希望保留这些文件,可以使用“persistentFake”方法。
门面
与传统的静态方法调用不同,门面 可以被模拟。这比传统的静态方法提供了很大的优势,并为您提供了与使用依赖注入相同的可测试性。在测试时,您可能经常希望模拟对控制器中 Laravel 门面的调用。例如,考虑以下控制器操作:
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Cache;
class UserController extends Controller
{
/**
* 显示应用程序的所有用户列表。
*
* @return Response
*/
public function index()
{
$value = Cache::get('key');
//
}
}
我们可以使用 shouldReceive
方法模拟对 Cache
门面的调用,该方法将返回一个 Mockery 模拟实例。由于门面实际上是由 Laravel 服务容器 解析和管理的,因此它们比典型的静态类具有更高的可测试性。例如,让我们模拟对 Cache
门面的 get
方法的调用:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
class UserControllerTest extends TestCase
{
public function testGetIndex()
{
Cache::shouldReceive('get')
->once()
->with('key')
->andReturn('value');
$response = $this->get('/users');
// ...
}
}
您不应模拟 Request
门面。相反,在运行测试时,将所需的输入传递给 HTTP 辅助方法,如 get
和 post
。同样,不要模拟 Config
门面,而是在测试中调用 Config::set
方法。