Laravel 模拟

简介

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

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


模拟对象

模拟对象可以通过 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 Facade 的 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...
                
                // Assert a specific type of job was dispatched meeting the given truth test...
        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 Facade 的 fake 方法来阻止事件监听器被执行,然后断言事件被分发,甚至检查接收的数据。使用 fake 方法时,测试代码执行后会进行断言:

<?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
{
    /**
     * Test order shipping.
     */
    public function testOrderShipping()
    {
        Event::fake();

        // Perform order shipping...

        // Assert a specific type of event was dispatched meeting the given truth test...
        Event::assertDispatched(function (OrderShipped $event) use ($order) {
            return $event->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()。

伪造事件子集

如果只想要为指定的事件集合伪造事件监听器,可以将它们传递到 fakefakeFor 方法:

/**
 * 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\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
{
    /**
     * Test order process.
     */
    public function testOrderProcess()
    {
        $order = Event::fakeFor(function () {
            $order = factory(Order::class)->create();

            Event::assertDispatched(OrderCreated::class);

            return $order;
        });

        // Events are dispatched as normal and observers will run ...
        $order->update([...]);
    }
}

伪造邮件

我们可以使用 Mail Facade 的 fake 方法阻止邮件发送,然后断言发送给用户的可邮寄类,甚至检查接收的数据。使用 fake 的时候,断言会在测试代码执行后进行:

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

        // Assert that no mailables were sent...
        Mail::assertNothingSent();

        // Perform order shipping...

        // Assert a specific type of mailable was dispatched meeting the given truth test...
        Mail::assertSent(function (OrderShipped $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 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();

        // Assert that no notifications were sent...
        Notification::assertNothingSent();

        // Perform order shipping...

        // Assert a specific type of notification was sent meeting the given truth test...
        Notification::assertSentTo(
            $user,
            function (OrderShipped $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 Facadefake 方法来阻止任务被推动到队列,然后断言任务是否被推送到队列,甚至检查接收的数据。使用 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 Facadefake 方法允许我们轻松构造伪造硬盘,以及使用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 方法。


Facade

不同于传统的静态方法调用,facade 可以被模拟。这与传统静态方法相比是一个巨大的优势,并且你可以对依赖注入进行测试。测试的时候,你可能经常想要在控制器中模拟 Laravel Facade 的调用,例如,看看下面的控制器动作:

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Cache;

class UserController extends Controller
{
    /**
     * Show a list of all users of the application.
     *
     * @return Response
     */
    public function index()
    {
        $value = Cache::get('key');

        //
    }
}

我们可以通过使用 shouldReceive 方法模拟 Cache Facade的调用,该方法返回一个Mockery模拟的实例,由于Facade通过 Laravel 服务容器进行解析和管理,所以它们比通常的静态类更具有可测试性。例如,我们可以来模拟 Cache Facade get 方法的调用:

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

        // ...
    }
}

注:不要模拟 Request Facade,取而代之地,可以在测试时传递期望输入到 HTTP 辅助函数如 getpost,类似地,也不要模拟 Config Facade,在测试中调用 Config::set 方法即可。

查看笔记

扫码一下
查看教程更方便