测试模拟器 Mocking

本文档最新版为 7.x,旧版本可能放弃维护,推荐阅读最新版!

测试模拟器

简介

在 Laravel 应用程序测试中,你可能希望「模拟」应用程序的某些功能的行为,从而避免该部分在测试中真正执行。例如:在控制器执行过程中会触发事件,从而避免该事件在测试控制器时真正执行。这允许你在仅测试控制器 HTTP 响应的情况时,而不必担心触发事件。当然,你也可以在单独的测试中测试该事件逻辑。

Laravel 针对事件、任务和 Facades 的模拟,提供了开箱即用的辅助函数。这些函数基于 Mocker 封装而成,使用非常方便,无需手动调用复杂的 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();
});

同样, 如果你想侦查一个对象, Laravel 的基本测试用例类提供了一个便捷的 spy 方法作为 Mockery::spy 的替代方法:

use App\Service;

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

任务模拟

作为模拟的替代方式,你可以使用 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();

        // 执行订单发货...

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

        // 断言任务并未分发...
        Bus::assertNotDispatched(AnotherJob::class);
    }
}

事件模拟

作为 mock 的替代方法,你可以使用 Event Facade 的 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
{
    /**
     * 测试订单发送
     */
    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() 之后 使用工厂模型。

模拟事件的子集

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

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

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

    Event::assertDispatched(OrderCreated::class);

    // 其他事件照常发送...
    $order->update([...]);
}

Scoped 事件模拟

如果你只想为部分测试模拟事件监听,则可以使用 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 Facade 的 fake 方法来模拟邮件发送,测试时不会真的发送邮件,然后你可以断言 mailables 发送给了用户,甚至可以检查他们收到的内容。使用 fakes 时,断言一般放在测试代码的后面:

<?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 Facade 的 fake 方法来模拟通知的发送,测试时并不会真的发出通知。然后你可以断言 notifications 发送给了用户,甚至可以检查他们收到的内容。使用 fakes 时,断言一般放在测试代码后面:

<?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 Facade 的 fake 方法避免把任务真的放到队列中执行。然后你就可以断言任务已经被推送入队列了,甚至可以检查它们收到的数据。使用 fakes 时,断言一般放在测试代码的后面:

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

        // 断言任务进入2次...
        Queue::assertPushed(ShipOrder::class, 2);

        // 断言没有一个任务进入队列...
        Queue::assertNotPushed(AnotherJob::class);

        // 断言任务是由特定的通道发送的..
        Queue::assertPushedWithChain(ShipOrder::class, [
            AnotherJob::class,
            FinalJob::class
        ]);
    }
}

存储模拟

你可以使用 Storage Facade 的 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']);
    }
}

Tip: 默认情况下,fake 方法将删除临时目录下所有文件。如果你想保留这些文件,你可以使用 「persistentFake」。

Facades

与传统静态方法调用不同的是,facades 也可以被模拟。相较传统的静态方法而言,它具有很大的优势,即便你使用依赖注入,可测试性不逊半分。在测试中,你可能想在控制器中模拟对 Laravel Facade 的调用。比如下面控制器中的行为:

<?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 Facade,此函数会返回一个 Mockery 实例。由于 Facade 的调用实际是由 Laravel 的 服务容器 管理的,所以 Facade 能比传统的静态类表现出更好的可测试性。下面,让我们模拟一下 Cache Facade 的 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 Facade 。相反,在运行测试时如果需要传入指定参数,请使用 HTTP 辅助函数,比如 getpost 。同理,请在测试时通过调用 Config::set 来模拟 Config Facade。

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
上一篇 下一篇
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
贡献者:3
讨论数量: 0
发起讨论 只看当前版本


暂无话题~