模拟
简介
测试 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();
}));
为了让代码更简洁,可以使用 Laravel 测试基类提供的 mock
方法:
use App\Service;
$this->mock(Service::class, function ($mock) {
$mock->shouldReceive('process')->once();
});
当你只需要模拟一个对象的少数方法时,可以使用 partialMock
方法,未被模拟的方法被执行时会正常调用:
use App\Service;
$this->partialMock(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
作为模拟的替代方案,你可以使用 Bus
门面的 fake
方法来阻止任务被分发,使用 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();
// Perform order shipping...
Bus::assertDispatched(ShipOrder::class, function ($job) use ($order) {
return $job->order->id === $order->id;
});
// Assert a job was not dispatched...
Bus::assertNotDispatched(AnotherJob::class);
}
}
伪造事件
作为模拟的替代方案,你可以使用 Event
门面的 fake
方法来阻止事件监听器被执行,然后断言事件被分发,甚至检查接收的数据。使用 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
{
/**
* Test order shipping.
*/
public function testOrderShipping()
{
Event::fake();
// Perform order shipping...
Event::assertDispatched(OrderShipped::class, function ($e) use ($order) {
return $e->order->id === $order->id;
});
// Assert an event was dispatched twice...
Event::assertDispatched(OrderShipped::class, 2);
// Assert an event was not dispatched...
Event::assertNotDispatched(OrderFailedToShip::class);
}
}
注:调用
Event:fake()
后,事件监听器不会执行,因此,如果你的测试使用了依赖于事件的模型工厂,例如在模型creating
事件中创建一个 UUID,那么你需要在使用工厂后再调用Event::fake()
。
伪造事件子集
如果你只想要为指定的事件集合伪造事件监听器,可以将它们传递到 fake
或 fakeFor
方法:
/**
* Test order process.
*/
public function testOrderProcess()
{
Event::fake([
OrderCreated::class,
]);
$order = factory(Order::class)->create();
Event::assertDispatched(OrderCreated::class);
// Other events are dispatched as normal...
$order->update([...]);
}
有作用域的事件伪造
如果你只想为部分测试伪造事件监听器,可以使用 fakeFor
方法:
<?php
namespace Tests\Feature;
use App\Models\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
{
/**
* Test order process.
*/
public function testOrderProcess()
{
$order = Event::fakeFor(function () {
$order = Order::factory()->create();
Event::assertDispatched(OrderCreated::class);
return $order;
});
// Events are dispatched as normal and observers will run ...
$order->update([...]);
}
}
伪造 HTTP
Http
门面的 fake
方法允许你告知 HTTP 客户端发起请求后返回示例响应,更多外部 HTTP 请求伪造明细,请参考 HTTP 客户端测试文档。
伪造邮件
你可以使用 Mail
门面的 fake
方法阻止邮件发送,然后断言发送给用户的可邮寄类,甚至检查接收的数据。使用 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();
// Assert that no mailables were sent...
Mail::assertNothingSent();
// Perform order shipping...
Mail::assertSent(OrderShipped::class, function ($mail) use ($order) {
return $mail->order->id === $order->id;
});
// Assert a message was sent to the given users...
Mail::assertSent(OrderShipped::class, function ($mail) use ($user) {
return $mail->hasTo($user->email) &&
$mail->hasCc('...') &&
$mail->hasBcc('...');
});
// Assert a mailable was sent twice...
Mail::assertSent(OrderShipped::class, 2);
// Assert a mailable was not sent...
Mail::assertNotSent(AnotherMailable::class);
}
}
如果你将邮件发送推送到了后台异步队列,需要使用 assertQueued
来替代 assertSent
:
Mail::assertQueued(...);
Mail::assertNotQueued(...);
伪造通知
你可以使用 Notification
门面的 fake
方法来阻止通知被发送,之后断言通知是否被发送给用户,甚至可以检查接收的数据。使用 fake
的时候,断言会在测试代码执行后进行:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Notifications\OrderShipped;
use Illuminate\Support\Facades\Notification;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
class ExampleTest extends TestCase
{
public function testOrderShipping()
{
Notification::fake();
// Assert that no notifications were sent...
Notification::assertNothingSent();
// Perform order shipping...
Notification::assertSentTo(
$user,
OrderShipped::class,
function ($notification, $channels) use ($order) {
return $notification->order->id === $order->id;
}
);
// Assert a notification was sent to the given users...
Notification::assertSentTo(
[$user], OrderShipped::class
);
// Assert a notification was not sent...
Notification::assertNotSentTo(
[$user], AnotherNotification::class
);
// Assert a notification was sent via Notification::route() method...
Notification::assertSentTo(
new AnonymousNotifiable, OrderShipped::class
);
// Assert Notification::route() method sent notification to the correct user...
Notification::assertSentTo(
new AnonymousNotifiable,
OrderShipped::class,
function ($notification, $channels, $notifiable) use ($user) {
return $notifiable->routes['mail'] === $user->email;
}
);
}
}
伪造队列
作为模拟的替代方案,你可以使用 Queue
门面的 fake
方法来阻止任务被推动到队列,然后断言任务是否被推送到队列,甚至检查接收的数据。使用 fake
的时候,断言会在测试代码执行后进行:
<?php
namespace Tests\Feature;
use App\Jobs\AnotherJob;
use App\Jobs\FinalJob;
use App\Jobs\ShipOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function testOrderShipping()
{
Queue::fake();
// Assert that no jobs were pushed...
Queue::assertNothingPushed();
// Perform order shipping...
// Assert a specific type of job was pushed meeting the given truth test...
Queue::assertPushed(function (ShipOrder $job) use ($order) {
return $job->order->id === $order->id;
});
// Assert a job was pushed to a given queue...
Queue::assertPushedOn('queue-name', ShipOrder::class);
// Assert a job was pushed twice...
Queue::assertPushed(ShipOrder::class, 2);
// Assert a job was not pushed...
Queue::assertNotPushed(AnotherJob::class);
// Assert a job was pushed with a given chain of jobs, matching by class...
Queue::assertPushedWithChain(ShipOrder::class, [
AnotherJob::class,
FinalJob::class
]);
// Assert a job was pushed with a given chain of jobs, matching by both class and properties...
Queue::assertPushedWithChain(ShipOrder::class, [
new AnotherJob('foo'),
new FinalJob('bar'),
]);
// Assert a job was pushed without a chain of jobs...
Queue::assertPushedWithoutChain(ShipOrder::class);
}
}
伪造存储
Storage
门面的 fake
方法允许你轻松构造伪造磁盘,以及使用UploadedFile
类生成的文件,从而极大简化了文件上传测试,例如:
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
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')
]);
// Assert one or more files were stored...
Storage::disk('photos')->assertExists('photo1.jpg');
Storage::disk('photos')->assertExists(['photo1.jpg', 'photo2.jpg']);
// Assert one or more files were not stored...
Storage::disk('photos')->assertMissing('missing.jpg');
Storage::disk('photos')->assertMissing(['missing.jpg', 'non-existing.jpg']);
}
}
注:默认情况下,
fake
方法会删除临时目录下的所有文件,如果你想要保留这些文件,可以使用persistentFake
方法。
与时间交互
在测试过程中,你可能偶尔需要修改 now
或者 Illuminate\Support\Carbon::now()
等辅助函数返回的时间,Laravel 测试基类中包含了允许你操纵当前时间的辅助函数:
public function testTimeCanBeManipulated()
{
// Travel into the future...
$this->travel(5)->milliseconds();
$this->travel(5)->seconds();
$this->travel(5)->minutes();
$this->travel(5)->hours();
$this->travel(5)->days();
$this->travel(5)->weeks();
$this->travel(5)->years();
// Travel into the past...
$this->travel(-5)->hours();
// Travel to an explicit time...
$this->travelTo(now()->subHours(6));
// Return back to the present time...
$this->travelBack();
}
门面
不同于传统的静态方法调用,门面可以被模拟。这与传统静态方法相比是一个巨大的优势,并且你可以对依赖注入进行测试。测试的时候,你可能经常想要在控制器中模拟 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
方法即可。
No Comments