在 Laravel 中运行功能测试时如何模拟服务(或服务提供者)?
Posted
技术标签:
【中文标题】在 Laravel 中运行功能测试时如何模拟服务(或服务提供者)?【英文标题】:How to mock a service (or a ServiceProvider) when running Feature tests in Laravel? 【发布时间】:2018-10-20 02:45:45 【问题描述】:我正在 Laravel 中编写一个小型 API,部分是为了学习这个框架。我想我在文档中发现了一个巨大的漏洞,但这可能是因为我不理解“Laravel 方式”来做我想做的事。
我正在编写一个 HTTP API,其中包括列出、创建和删除 Linux 服务器上的系统用户。结构是这样的:
到/v1/users
的路由将GET
、POST
和DELETE
动词分别连接到控制器方法get
、create
和delete
。
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 来代替?
【问题讨论】:
为什么要测试服务商?服务提供者有两个函数boot()
和register()
,有什么要测试的。它都在 Laravel 测试中进行了测试。您需要测试服务提供者实例化的类。
@Kyslik:我不想测试服务提供商(或服务)。我只想将服务换成虚拟服务,这样我就可以使用虚拟服务测试系统(在这种情况下使用真实服务将使其成为集成测试,我只想要一个独立的功能测试)。
【参考方案1】:
显而易见的方法是在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 或其他)在@987654326 @方法
if (app()->environment('testing'))
app()->register(FakeUserProvider::class);
else
app()->register(UserProvider::class);
注意:缺点是提供者的 list 位于两个位置,一个在 config/app.php 中,一个在 AppServiceProvider.php 中
【讨论】:
哈哈,和我的很像,刚刚发的!这次真是万分感谢。我通过自动完成和猜测的混合获得了我的,而不是来之不易的经验:-)
。我会看看将我的绑定移动到setUp()
,而不是在每个测试中都这样做 - 好主意。
@halfer 是的,我明白了,干得好! PHPStorm 摇滚 :)。这样做,按照我的建议创建 UserTestCase 并在其中包含setUp()
。这样做的问题是,当您需要多个 重新绑定 时,您最终会混淆,所以我建议使用条件注册提供程序。
我去看看,谢谢。我对函数app()
很感兴趣——它是一个全局函数吗,在某种程度上与使用$this->app
有什么不同吗?裸函数在课堂上对我来说有点奇怪,但我想知道这是否是我只需要适应的 Laravel 东西。
@halfer 是的,它是 Laravel 的东西,有很多帮助者实际上可以提供帮助 :) 看到这个 file。例如,我不使用app()->make(A::class)
,而是使用resolve(A::class)
,即app(A::class)
,或者有时当我不注入Request
时,我只使用request()
(如果用户已登录,我可以通过request()->user()
访问实例),或abort(...)
,或route('route.name.here')
...我有时会创建自己的 helper.php 文件并自动加载它,就像 Laravel 一样。
@halfer 如果你真的是 Laravel 的新手,请在 laracasts.com 上获取一些免费课程 - Laravel 的新功能 非常好或 Laravel From Scratch - 我不是确定什么是免费/付费的。祝你好运。【参考方案2】:
啊哈,我找到了一个临时解决方案。我会把它贴在这里,然后解释如何改进它。
<?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
) 更改为接口,这样我的测试实现就不需要从真实的扩展。
【讨论】:
以上是关于在 Laravel 中运行功能测试时如何模拟服务(或服务提供者)?的主要内容,如果未能解决你的问题,请参考以下文章
如何在 PHPUnit / Laravel 中模拟 JSON 文件