如何在Laravel中运行功能测试时模拟服务(或ServiceProvider)?

时间:2018-05-09 21:50:58

标签: php laravel laravel-5 mocking phpunit

我正在Laravel中编写一个小API,部分原因是为了学习这个框架。我想我已经在文档中发现了一个漏洞,但这可能是因为我不理解“Laravel方式”来做我想做的事。

我正在编写一个HTTP API,其中包括在Linux服务器上列出,创建和删除系统用户。结构是这样的:

  • 路由到/v1/users分别将GETPOSTDELETE动词连接到控制器方法getcreatedelete
  • 控制器App\Http\Controllers\UserController实际上并不运行系统调用,而是由服务App\Services\Users完成。
  • 该服务由ServiceProvider App\Providers\Server\Users创建,该服务在延迟的基础上注册singleton服务。
  • 该服务由Laravel自动实例化并自动注入控制器的构造函数。

好的,所以这一切都有效。我也写了一些测试代码,如下:

public function testGetUsers()
{
    $response = $this->json('GET', '/v1/users');
    /* @var $response \Illuminate\Http\JsonResponse */

    $response
        ->assertStatus(200)
        ->assertJson(['ok' => true, ]);
}

这也很好。但是,这使用UserService的常规绑定,我想在这里放置一个虚拟/模拟。

我想我需要将UserService更改为界面,这很容易,但我不知道如何告诉底层测试系统我希望它运行我的控制器,但是非标准服务。我在研究这个问题时看到了App::bind()在Stack Overflow答案中出现,但是App并不是在工匠生成的测试中自动调整范围,所以感觉就像抓着稻草一样。

如何实例化虚拟服务,然后在测试时将其发送到Laravel,因此它不使用标准的ServiceProvider?

2 个答案:

答案 0 :(得分:7)

显而易见的方法是在setUp()中重新绑定实现。

让自己成为新的UserTestCase(或编辑Laravel提供的那个)并添加:

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;

    protected function setUp()
    {
        parent::setUp();

        app()->bind(YourService::class, function() { // not a service provider but the target of service provider
            return new YourFakeService();
        });
    }
}

class YourFakeService {} // I personally keep fakes in the test files itself if they are short

根据环境有条件地注册提供商(将其放在 AppServiceProvider.php 或您为此任务指定的任何其他提供商 - ConditionalLoaderServiceProvider.php 或其他){{ 1}}方法

register()
  

注意:缺点是提供商的 list 位于config / app.php中的两个位置,AppServiceProvider.php中的一个位置

答案 1 :(得分:3)

啊哈,我找到了一个临时解决方案。我将在此发布,然后解释如何改进它。

<?php

namespace Tests\Feature;

use Tests\TestCase;
use \App\Services\Users as UsersService;

class UsersTest extends TestCase
{
    /**
     * Checks the listing of users
     *
     * @return void
     */
    public function testGetUsers()
    {
        $this->app->bind(UsersService::class, function() {
            return new UsersDummy();
        });

        $response = $this->json('GET', '/v1/users');

        $response
            ->assertStatus(200)
            ->assertJson(['ok' => true, ]);
    }
}

class UsersDummy extends UsersService
{
    public function listUsers()
    {
        return ['tom', 'dick', 'harry', ];
    }
}

这会注入一个DI绑定,因此默认的ServiceProvider不需要启动。如果我将一些调试代码添加到$response,如下所示:

/* @var $response \Illuminate\Http\JsonResponse */
print_r($response->getData(true));

然后我得到了这个输出:

Array
(
    [ok] => 1
    [users] => Array
        (
            [0] => tom
            [1] => dick
            [2] => harry
        )

)

这使我能够创建一个带有围绕PHP绘制边界的测试,并且不会对测试框进行调用以与用户系统进行交互。

接下来我将研究我的控制器构造函数是否可以从具体的实现提示(\App\Services\Users)更改为接口,这样我的测试实现就不需要从实际的扩展中扩展。 / p>