Skip to content

模拟

介绍

在测试 Laravel 应用程序时,您可能希望“模拟”应用程序的某些方面,以便在给定测试期间不实际执行它们。例如,在测试调度事件的控制器时,您可能希望模拟事件监听器,以便在测试期间不实际执行它们。这使您可以仅测试控制器的 HTTP 响应,而不必担心事件监听器的执行,因为事件监听器可以在自己的测试用例中进行测试。

Laravel 提供了用于模拟事件、作业和门面的助手。这些助手主要提供了一个便利层,覆盖了 Mockery,因此您不必手动进行复杂的 Mockery 方法调用。您还可以使用 Mockery 或 PHPUnit 创建自己的模拟或间谍。

模拟对象

当模拟一个将通过 Laravel 的服务容器注入到您的应用程序中的对象时,您需要将您的模拟实例绑定到容器中作为 instance 绑定。这将指示容器使用您的模拟对象实例,而不是自行构造对象:

php
use App\Service;
use Mockery;

$this->instance(Service::class, Mockery::mock(Service::class, function ($mock) {
    $mock->shouldReceive('process')->once();
}));

为了使这更方便,您可以使用 mock 方法,这是 Laravel 的基础测试用例类提供的:

php
use App\Service;

$this->mock(Service::class, function ($mock) {
    $mock->shouldReceive('process')->once();
});

当您只需要模拟对象的几个方法时,可以使用 partialMock 方法。未模拟的方法在调用时将正常执行:

php
use App\Service;

$this->partialMock(Service::class, function ($mock) {
    $mock->shouldReceive('process')->once();
});

同样,如果您想监视一个对象,Laravel 的基础测试用例类提供了一个 spy 方法,作为 Mockery::spy 方法的便捷包装:

php
use App\Service;

$this->spy(Service::class, function ($mock) {
    $mock->shouldHaveReceived('process');
});

总线假对象

作为模拟的替代方案,您可以使用 Bus 门面的 fake 方法来防止作业被调度。在使用假对象时,断言是在测试代码执行后进行的:

php
<?php

namespace Tests\Feature;

use App\Jobs\ShipOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Bus;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function testOrderShipping()
    {
        Bus::fake();

        // 执行订单发货...

        // 断言调度了特定类型的作业,满足给定的真值测试...
        Bus::assertDispatched(function (ShipOrder $job) use ($order) {
            return $job->order->id === $order->id;
        });

        // 断言未调度作业...
        Bus::assertNotDispatched(AnotherJob::class);
    }
}

事件假对象

作为模拟的替代方案,您可以使用 Event 门面的 fake 方法来防止所有事件监听器执行。然后,您可以断言事件已被调度,甚至检查它们接收到的数据。在使用假对象时,断言是在测试代码执行后进行的:

php
<?php

namespace Tests\Feature;

use App\Events\OrderFailedToShip;
use App\Events\OrderShipped;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * 测试订单发货。
     */
    public function testOrderShipping()
    {
        Event::fake();

        // 执行订单发货...

        // 断言调度了特定类型的事件,满足给定的真值测试...
        Event::assertDispatched(function (OrderShipped $event) use ($order) {
            return $event->order->id === $order->id;
        });

        // 断言事件被调度了两次...
        Event::assertDispatched(OrderShipped::class, 2);

        // 断言未调度事件...
        Event::assertNotDispatched(OrderFailedToShip::class);
    }
}
exclamation

调用 Event::fake() 后,不会执行任何事件监听器。因此,如果您的测试使用依赖于事件的模型工厂,例如在模型的 creating 事件期间创建 UUID,您应该在使用工厂后调用 Event::fake()

模拟一部分事件

如果您只想为特定事件集模拟事件监听器,可以将它们传递给 fakefakeFor 方法:

php
/**
 * 测试订单处理。
 */
public function testOrderProcess()
{
    Event::fake([
        OrderCreated::class,
    ]);

    $order = factory(Order::class)->create();

    Event::assertDispatched(OrderCreated::class);

    // 其他事件正常调度...
    $order->update([...]);
}

作用域事件假对象

如果您只想为测试的一部分模拟事件监听器,可以使用 fakeFor 方法:

php
<?php

namespace Tests\Feature;

use App\Events\OrderCreated;
use App\Order;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * 测试订单处理。
     */
    public function testOrderProcess()
    {
        $order = Event::fakeFor(function () {
            $order = factory(Order::class)->create();

            Event::assertDispatched(OrderCreated::class);

            return $order;
        });

        // 事件正常调度,观察者将运行...
        $order->update([...]);
    }
}

HTTP 假对象

Http 门面的 fake 方法允许您指示 HTTP 客户端在请求时返回存根/虚拟响应。有关伪造传出 HTTP 请求的更多信息,请查阅 HTTP 客户端测试文档

邮件假对象

您可以使用 Mail 门面的 fake 方法来防止邮件发送。然后,您可以断言 邮件 已发送给用户,甚至检查它们接收到的数据。在使用假对象时,断言是在测试代码执行后进行的:

php
<?php

namespace Tests\Feature;

use App\Mail\OrderShipped;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function testOrderShipping()
    {
        Mail::fake();

        // 断言未发送任何邮件...
        Mail::assertNothingSent();

        // 执行订单发货...

        // 断言调度了特定类型的邮件,满足给定的真值测试...
        Mail::assertSent(function (OrderShipped $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

php
Mail::assertQueued(...);
Mail::assertNotQueued(...);

通知假对象

您可以使用 Notification 门面的 fake 方法来防止通知发送。然后,您可以断言 通知 已发送给用户,甚至检查它们接收到的数据。在使用假对象时,断言是在测试代码执行后进行的:

php
<?php

namespace Tests\Feature;

use App\Notifications\OrderShipped;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Notifications\AnonymousNotifiable;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function testOrderShipping()
    {
        Notification::fake();

        // 断言未发送任何通知...
        Notification::assertNothingSent();

        // 执行订单发货...

        // 断言发送了特定类型的通知,满足给定的真值测试...
        Notification::assertSentTo(
            $user,
            function (OrderShipped $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
        );

        // 断言 Notification::route() 方法将通知发送给了正确的用户...
        Notification::assertSentTo(
            new AnonymousNotifiable,
            OrderShipped::class,
            function ($notification, $channels, $notifiable) use ($user) {
                return $notifiable->routes['mail'] === $user->email;
            }
        );
    }
}

队列假对象

作为模拟的替代方案,您可以使用 Queue 门面的 fake 方法来防止作业被排队。然后,您可以断言作业已被推送到队列,甚至检查它们接收到的数据。在使用假对象时,断言是在测试代码执行后进行的:

php
<?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();

        // 断言未推送任何作业...
        Queue::assertNothingPushed();

        // 执行订单发货...

        // 断言推送了特定类型的作业,满足给定的真值测试...
        Queue::assertPushed(function (ShipOrder $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
        ]);

        // 断言作业被推送,并具有给定的作业链,按类和属性匹配...
        Queue::assertPushedWithChain(ShipOrder::class, [
            new AnotherJob('foo'),
            new FinalJob('bar'),
        ]);

        // 断言作业被推送,没有作业链...
        Queue::assertPushedWithoutChain(ShipOrder::class);
    }
}

存储假对象

Storage 门面的 fake 方法允许您轻松生成一个假磁盘,结合 UploadedFile 类的文件生成工具,大大简化了文件上传的测试。例如:

php
<?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')
        ]);

        // 断言一个或多个文件已存储...
        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']);
    }
}
lightbulb

默认情况下,fake 方法将删除其临时目录中的所有文件。如果您希望保留这些文件,可以使用“persistentFake”方法。

门面

与传统的静态方法调用不同,门面 可以被模拟。这比传统的静态方法提供了很大的优势,并为您提供了与使用依赖注入相同的可测试性。在测试时,您可能经常希望在控制器中模拟对 Laravel 门面的调用。例如,考虑以下控制器操作:

php
<?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
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Cache;
use Tests\TestCase;

class UserControllerTest extends TestCase
{
    public function testGetIndex()
    {
        Cache::shouldReceive('get')
                    ->once()
                    ->with('key')
                    ->andReturn('value');

        $response = $this->get('/users');

        // ...
    }
}
exclamation

您不应模拟 Request 门面。相反,在运行测试时,将所需的输入传递给 HTTP 辅助方法,如 getpost。同样,您不应模拟 Config 门面,而应在测试中调用 Config::set 方法。