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

xghobddn  于 2023-05-08  发布在  其他
关注(0)|答案(3)|浏览(91)

我正在用Laravel写一个小的API,部分是为了学习这个框架。我想我在文档中发现了一个巨大的漏洞,但这可能是由于我不理解“Laravel方式”来做我想做的事情。
我正在编写一个HTTP API,用于在Linux服务器上列出、创建和删除系统用户。结构是这样的:

  • /v1/users的路由将GETPOSTDELETE动词分别连接到控制器方法getcreatedelete
  • 控制器App\Http\Controllers\UserController实际上并不运行系统调用,这是由服务App\Services\Users完成的。
  • 该服务由ServiceProvider App\Providers\Server\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的普通绑定,我想在这里放置一个dummy/mock。
我想我需要将我的UserService更改为一个接口,这很容易,但我不确定如何告诉底层测试系统我希望它运行我的控制器,但使用非标准服务。在研究这个问题时,我看到App::bind()出现在Stack Overflow的答案中,但是App并没有自动出现在artisan生成的测试中,所以感觉就像抓住了救命稻草。
我如何示例化一个虚拟服务,然后在测试时将其发送到Laravel,这样它就不会使用标准的ServiceProvider了?

hfyxw5xn

hfyxw5xn1#

最明显的方法是在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

register()方法中基于环境有条件地注册提供程序(将其放入AppServiceProvider.php或您为此任务指定的任何其他提供程序-ConditionalLoaderServiceProvider.php或其他)

if (app()->environment('testing')) {
    app()->register(FakeUserProvider::class);
} else {
    app()->register(UserProvider::class);
}

注意:缺点是 list 的提供者在两个地方,一个在config/app.php,一个在AppServiceProvider.php

hjqgdpho

hjqgdpho2#

啊哈,我找到了一个临时的解决办法。我会把它贴在这里,然后解释如何改进它。

<?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)更改为接口,这样我的测试实现就不需要从真实的的实现扩展。

bq3bfh9z

bq3bfh9z3#

在Laravel“Feature”测试中,你可以使用以下方法来模拟(改变正常绑定):

use Mockery\MockInterface;

...

$this->mock(YourService::class, function (MockInterface $mock) {
           $mock
               ->shouldReceive('increase')  // Sample
               ->withArgs([1,4])            // Sample
               ->times(1)                   // Sample
               ->andReturn(3);              // Sample              
        });

那么你就不会从服务容器中得到“YourService”的正常具体实现,你将得到一个模拟的实现。

相关问题