测试模拟器

未匹配的标注
本文档最新版为 11.x,旧版本可能放弃维护,推荐阅读最新版!

Mocking

简介

在测试 Laravel 应用时,你可能希望「模拟(mock)」应用的某些部分,防止它们在测试过程中实际执行。例如,当你测试某个会分发事件的控制器时,你可能并不希望事件监听器在测试中真正运行。这样做的好处是,你可以专注于测试控制器的 HTTP 响应,而把事件监听器的行为放在独立的测试用例中验证。

Laravel 提供了便捷的辅助方法,用于模拟事件、任务(job)、以及其他 Facade。底层这些功能仍依赖 Mockery,只不过 Laravel 对其进行了包装,简化了原本复杂的模拟调用。

模拟对象

当你需要模拟一个对象,并通过 Laravel 的 服务容器 注入到应用中时,必须将模拟对象绑定为容器实例(instance。这样,Laravel 在依赖注入时就会使用你提供的模拟对象,而不是自动构造新的对象:

use App\Service;
use Mockery;
use Mockery\MockInterface;

test('something can be mocked', function () {
    $this->instance(
        Service::class,
        Mockery::mock(Service::class, function (MockInterface $mock) {
            $mock->expects('process');
        })
    );
});
use App\Service;
use Mockery;
use Mockery\MockInterface;

public function test_something_can_be_mocked(): void
{
    $this->instance(
        Service::class,
        Mockery::mock(Service::class, function (MockInterface $mock) {
            $mock->expects('process');
        })
    );
}

为了进一步简化模拟操作,Laravel 的基础测试类提供了一个 mock 方法,可以更方便地创建模拟对象。下面这个例子与前面的绑定 instance 的示例效果相同:

use App\Service;
use Mockery\MockInterface;

$mock = $this->mock(Service::class, function (MockInterface $mock) {
    $mock->expects('process');
});

如果你只需要模拟某个对象的部分方法,而希望其他方法在调用时照常执行,则可以使用 partialMock 方法:

use App\Service;
use Mockery\MockInterface;

$mock = $this->partialMock(Service::class, function (MockInterface $mock) {
    $mock->expects('process');
});

此外,如果你希望对一个对象进行 监视(spy),Laravel 的基础测试类也提供了便捷的 spy 方法,它是 Mockery::spy 的封装。Spy 与 Mock 类似,不同之处在于它会记录对象在测试过程中的调用信息,从而让你可以在代码执行完毕后再进行断言:

use App\Service;

$spy = $this->spy(Service::class);

// ...

$spy->shouldHaveReceived('process');

模拟 Facade

与传统的静态方法调用不同,Facade(包括 实时 Facade)是可以被模拟的。相较于传统的静态调用方式,这种做法带来了极大的测试优势,你可以像使用依赖注入一样灵活地进行测试。

在编写测试时,你可能会希望模拟控制器中调用的某个 Laravel Facade。例如,假设你有如下控制器动作:

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Cache;

class UserController extends Controller
{
    /**
     * 获取应用中的所有用户列表。
     */
    public function index(): array
    {
        $value = Cache::get('key');

        return [
            // ...
        ];
    }
}

我们可以使用 expects 方法来模拟对 Cache Facade 的调用。该方法会返回一个 Mockery 的模拟对象。由于 Laravel 中的 Facade 实际上是由 服务容器 解析与管理的,因此相比传统的静态类,它们拥有更高的可测试性。

例如,以下示例模拟了调用 Cache Facade 的 get 方法:

<?php

use Illuminate\Support\Facades\Cache;

test('get index', function () {
    Cache::expects('get')
        ->with('key')
        ->andReturn('value');

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

    // ...
});
<?php

namespace Tests\Feature;

use Illuminate\Support\Facades\Cache;
use Tests\TestCase;

class UserControllerTest extends TestCase
{
    public function test_get_index(): void
    {
        Cache::expects('get')
            ->with('key')
            ->andReturn('value');

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

        // ...
    }
}

⚠️ 警告
不建议模拟 Request Facade。如果你需要特定输入,应直接传入相应的参数到测试方法中,如 getpost 方法。
同理,也不应模拟 Config Facade,请使用 Config::set 方法设置配置值。

Facade 监视器

如果你想对某个 Facade 进行 监视(spy),可以直接在对应的 Facade 上调用 spy 方法。Spy 与 Mock 类似,但它会记录代码与该对象之间的所有交互,从而允许你在执行完逻辑后再进行断言:

<?php

use Illuminate\Support\Facades\Cache;

test('values are be stored in cache', function () {
    Cache::spy();

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

    $response->assertStatus(200);

    Cache::shouldHaveReceived('put')->with('name', 'Taylor', 10);
});
use Illuminate\Support\Facades\Cache;

public function test_values_are_be_stored_in_cache(): void
{
    Cache::spy();

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

    $response->assertStatus(200);

    Cache::shouldHaveReceived('put')->with('name', 'Taylor', 10);
}

与时间交互

在编写测试时,有时你可能需要修改辅助函数如 now 或 Illuminate\Support\Carbon::now() 返回的当前时间。Laravel 的基础测试类提供了便捷的方法来操控当前时间:

test('time can be manipulated', function () {
    // 向未来穿越...
    $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();

    // 向过去穿越...
    $this->travel(-5)->hours();

    // 穿越到指定时间...
    $this->travelTo(now()->subHours(6));

    // 返回现在...
    $this->travelBack();
});
public function test_time_can_be_manipulated(): void
{
    // 向未来穿越...
    $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();

    // 向过去穿越...
    $this->travel(-5)->hours();

    // 穿越到指定时间...
    $this->travelTo(now()->subHours(6));

    // 返回现在...
    $this->travelBack();
}

你也可以为时间旅行方法传入一个闭包,Laravel 会在指定时间“冻结”时间,然后执行闭包中的代码。一旦闭包执行完毕,时间将恢复正常流动:

$this->travel(5)->days(function () {
    // 测试未来五天后的某些操作...
});

$this->travelTo(now()->subDays(10), function () {
    // 测试过去十天前某一刻的行为...
});

你还可以使用 freezeTime 方法冻结当前时间;如果只希望冻结到当前秒的起始位置,则可以使用 freezeSecond 方法:

use Illuminate\Support\Carbon;

// 冻结时间,在闭包执行后恢复...
$this->freezeTime(function (Carbon $time) {
    // ...
});

// 冻结到当前秒的起始位置,闭包执行后恢复...
$this->freezeSecond(function (Carbon $time) {
    // ...
})

当然,前文提到的所有方法,主要都是用于测试与时间有关的应用行为,比如:在论坛中锁定一周内未活跃的帖子:

use App\Models\Thread;

test('forum threads lock after one week of inactivity', function () {
    $thread = Thread::factory()->create();

    $this->travel(1)->week();

    expect($thread->isLockedByInactivity())->toBeTrue();
});
use App\Models\Thread;

public function test_forum_threads_lock_after_one_week_of_inactivity()
{
    $thread = Thread::factory()->create();

    $this->travel(1)->week();

    $this->assertTrue($thread->isLockedByInactivity());
}

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

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/laravel/12.x/mo...

译文地址:https://learnku.com/docs/laravel/12.x/mo...

上一篇 下一篇
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
贡献者:2
讨论数量: 0
发起讨论 只看当前版本


暂无话题~