[ Laravel 5.3 文档 ] 测试 —— 模拟

1、简介

测试 Laravel 应用的时候,你可能还想要“ 模拟 ”应用的特定状态,以便在测试中不让它们真的执行。例如,测试触发事件的控制器时,你可能想要模拟事件监听器以便它们不在测试期间真的执行。这样的话你就可以只测试控制器的 HTTP 响应,而不必担心事件监听器的执行,因为事件监听器可以在它们自己的测试用例中被测试。

Laravel 开箱为模拟事件、任务以及工厂提供了辅助函数,这些辅助函数主要是在 Mockery 之上提供了一个方便的层这样你就不必手动调用复杂的 Mockery 方法。当然,你也可以使用 Mockery 或 PHPUnit 来创建自己的模拟。

2、事件

使用Mocks

如果你在重度使用Laravel的事件系统,可能想要在测试时模拟特定事件。例如,如果你在测试用户注册,你可能不想所有UserRegistered事件的处理器都被触发,因为这可能会发送欢迎邮件,等等。

Laravel提供了一个方便的expectsEvents方法来验证期望的事件被触发,但同时阻止该事件的其它处理器运行:

<?php

use App\Events\UserRegistered;

class ExampleTest extends TestCase
{
    /**
     * Test new user registration.
     */
    public function testUserRegistration()
    {
        $this->expectsEvents(UserRegistered::class);

        // Test user registration...
    }
}

可以使用doesntExpectEvents方法来验证给定事件没有被触发:

<?php

use App\Events\OrderShipped;
use App\Events\OrderFailedToShip;

class ExampleTest extends TestCase
{
    /**
     * Test order shipping.
     */
    public function testOrderShipping()
    {
        $this->expectsEvents(OrderShipped::class);
        $this->doesntExpectEvents(OrderFailedToShip::class);

        // Test order shipping...
    }
}

如果你想要阻止所有事件运行,可以使用withoutEvents方法,当这个方法被调用时,所有事件的监听器都会被模拟:

<?php

class ExampleTest extends TestCase{
    public function testUserRegistration()
    {
        $this->withoutEvents();
        // 测试用户注册代码...
    }
}

使用Fakes

作为模拟的可选方案,你可以使用 Event 门面fake 方法来阻止所有事件监听器的执行。你可以断言事件被触发甚至检查它们接收的数据。使用fake的时候,断言在测试代码执行后执行:

<?php

use App\Events\OrderShipped;
use App\Events\OrderFailedToShip;
use Illuminate\Support\Facades\Event;

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

        // 执行订单发货...

        Event::assertFired(OrderShipped::class, function ($e) use ($order) {
            return $e->order->id === $order->id;
        });

        Event::assertNotFired(OrderFailedToShip::class);
    }
}

3、任务

使用Mocks

有时候,你可能想要在请求时简单测试控制器分发的指定任务,这允许你孤立的测试路由/控制器——将其从任务逻辑中分离出去,当然,接下来你可以在一个独立测试类中测试任务本身。

Laravel提供了一个方便的expectsJobs方法来验证期望的任务被分发,但该任务本身不会被测试:

<?php

use App\Jobs\ShipOrder;

class ExampleTest extends TestCase
{
    public function testOrderShipping()
    {
        $this->expectsJobs(ShipOrder::class);

        // Test order shipping...
    }
}

注:这个方法只检查通过DispatchesJobs trait分发方法或辅助函数 dispatch 分发的任务,并不检查直接通过Queue::push分发的任务。

和事件模拟辅助函数一样,你还可以使用 doesntExpectJobs 方法测试一个任务没有被分发:

<?php

use App\Jobs\ShipOrder;

class ExampleTest extends TestCase
{
    /**
     * Test order cancellation.
     */
    public function testOrderCancellation()
    {
        $this->doesntExpectJobs(ShipOrder::class);

        // Test order cancellation...
    }
}

或者,你还可以使用 withoutJobs 方法忽略所有分发任务。当该方法在测试方法中被调用时,所有该测试期间被分发的任务都会被废弃:

<?php

use App\Jobs\ShipOrder;

class ExampleTest extends TestCase
{
    /**
     * 测试订单取消
     */
    public function testOrderCancellation()
    {
        $this->withoutJobs();

        // 测试订单取消...
    }
}

使用Fakes

作为mock的可选方案,你可以使用 Queue 门面的 fake 方法来阻止任务被推送到队列。之后你可以断言任务是否被推送到队列。甚至检查接收的数据,使用fakes的时候,断言会在测试代码执行后进行:

<?php

use App\Jobs\ShipOrder;
use Illuminate\Support\Facades\Queue;

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

        // 订单发货...

        Queue::assertPushed(ShipOrder::class, function ($job) use ($order) {
            return $job->order->id === $order->id;
        });

        // 断言任务是否被推送到给定队列...
        Queue::assertPushedOn('queue-name', ShipOrder::class);

        // 断言任务未被推送...
        Queue::assertNotPushed(AnotherJob::class);
    }
}

4、邮件Fakes

你可以使用 Mail 门面的 fake 方法阻止邮件被发送。然后断言邮件是否被发送到用户,甚至可以检查接收的数据,使用fakes的时候,断言会在测试代码执行后进行:

<?php

use App\Mail\OrderShipped;
use Illuminate\Support\Facades\Mail;

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

        // 订单发货...

        Mail::assertSent(OrderShipped::class, function ($mail) use ($order) {
            return $mail->order->id === $order->id;
        });

        // 断言消息被发送到给定用户...
        Mail::assertSentTo([$user], OrderShipped::class);

        // 断言邮件未被发送...
        Mail::assertNotSent(AnotherMailable::class);
    }
}

5、通知Fakes

你可以使用 Notification 门面的 fake 方法来阻止通知被发送,之后断言通知是否被发送给用户,甚至可以检查接收的数据。使用 fakes 的时候,断言会在测试代码执行后进行:

<?php

use App\Notifications\OrderShipped;
use Illuminate\Support\Facades\Notification;

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

        // 订单发货...

        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
        );
    }
}

6、门面

不同于传统的静态方法调用,门面可以被模拟。这与传统静态方法相比是一个巨大的优势,并且你可以对依赖注入进行测试。测试的时候,你可能经常想要在控制器中模拟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

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

        $this->visit('/users')->see('value');
    }
}

注:不要模拟 Request门面,取而代之地,可以在测试时传递期望输入到 HTTP 辅助函数如callpost

学院君

学院君 has written 548 articles

资深PHP工程师,Laravel学院院长