我有一个控制器,我想为其创建功能测试。该控制器通过MyApiClient
类向外部API发出HTTP请求。我需要模拟这个MyApiClient
类,所以我可以测试我的控制器如何响应给定的响应(例如,如果MyApiClient
类返回500响应,它会怎么做。)
通过标准的PHPUnit mockbuilder创建MyApiClient
类的模拟版本没有问题:我遇到的问题是让我的控制器将此对象用于多个请求。
我目前正在测试中执行以下操作:
class ApplicationControllerTest extends WebTestCase
{
public function testSomething()
{
$client = static::createClient();
$apiClient = $this->getMockMyApiClient();
$client->getContainer()->set('myapiclient', $apiClient);
$client->request('GET', '/my/url/here');
// Some assertions: Mocked API client returns 500 as expected.
$client->request('GET', '/my/url/here');
// Some assertions: Mocked API client is not used: Actual MyApiClient instance is being used instead.
}
protected function getMockMyApiClient()
{
$client = $this->getMockBuilder('Namespace\Of\MyApiClient')
->setMethods(array('doSomething'))
->getMock();
$client->expects($this->any())
->method('doSomething')
->will($this->returnValue(500));
return $apiClient;
}
}
似乎在第二次请求时正在重建容器,导致MyApiClient
再次被实例化。 MyApiClient
类通过注释(使用JMS DI Extra Bundle)配置为服务,并通过注释注入控制器的属性。
我将每个请求拆分到自己的测试中,如果可以的话,可以解决这个问题,但不幸的是我不能:我需要通过GET操作向控制器发出请求,然后POST它带来的表单背部。我想这样做有两个原因:
1)表单使用CSRF保护,因此如果我直接在不使用爬虫提交的情况下直接发布到表单,表单将无法通过CSRF检查。
2)测试表单在提交时生成正确的POST请求是一种奖励。
有没有人对如何做到这一点有任何建议?
修改
这可以在以下单元测试中表示,该测试不依赖于我的任何代码,因此可能更清楚:
public function testAMockServiceCanBeAccessedByMultipleRequests()
{
$client = static::createClient();
// Set the container to contain an instance of stdClass at key 'testing123'.
$keyName = 'testing123';
$client->getContainer()->set($keyName, new \stdClass());
// Check our object is still set on the container.
$this->assertEquals('stdClass', get_class($client->getContainer()->get($keyName))); // Passes.
$client->request('GET', '/any/url/');
$this->assertEquals('stdClass', get_class($client->getContainer()->get($keyName))); // Passes.
$client->request('GET', '/any/url/');
$this->assertEquals('stdClass', get_class($client->getContainer()->get($keyName))); // Fails.
}
即使我在第二次拨打$client->getContainer()->set($keyName, new \stdClass());
request()
,此测试也会失败
答案 0 :(得分:8)
我以为我会跳进这里。克里斯克,我想你想要的是:
https://github.com/PolishSymfonyCommunity/SymfonyMockerContainer
我同意您的一般方法,在服务容器中将其配置为参数确实不是一个好方法。整个想法是能够在单个测试运行期间动态模拟它。
答案 1 :(得分:8)
当您调用self::createClient()
时,您将获得Symfony2内核的启动实例。这意味着,解析并加载所有配置。当现在发送请求时,你让系统第一次完成它的工作,对吗?
在第一次请求之后,您可能想要检查发生了什么,因此,内核处于发送请求的状态,但它仍在运行。
如果您现在运行第二个请求,则Web架构要求内核重新启动,因为它已经运行了请求。当您第二次执行请求时,将执行代码中的重新启动。
如果要在发送请求之前启动内核并对其进行修改(如您所愿),则必须关闭旧内核实例并启动新内核实例。
只需重新运行self::createClient()
即可。现在你再次必须像第一次那样应用你的模拟。
这是您的第二个示例的修改代码:
public function testAMockServiceCanBeAccessedByMultipleRequests()
{
$keyName = 'testing123';
$client = static::createClient();
$client->getContainer()->set($keyName, new \stdClass());
// Check our object is still set on the container.
$this->assertEquals('stdClass', get_class($client->getContainer()->get($keyName)));
$client->request('GET', '/any/url/');
$this->assertEquals('stdClass', get_class($client->getContainer()->get($keyName)));
# addded these two lines here:
$client = static::createClient();
$client->getContainer()->set($keyName, new \stdClass());
$client->request('GET', '/any/url/');
$this->assertEquals('stdClass', get_class($client->getContainer()->get($keyName)));
}
现在您可能想要创建一个单独的方法,为您模拟新鲜实例,这样您就不必复制代码......
答案 2 :(得分:2)
您遇到的行为实际上是您在任何真实场景中所经历的行为,因为PHP无需任何共享,并在每个请求上重建整个堆栈。功能测试套件模仿此行为不会产生错误的结果。一个例子是doctrine,它有一个ObjectCache,因此您可以创建对象,而不是将它们保存到数据库中,并且您的测试都会通过,因为它始终将对象从缓存中取出。
您可以通过不同方式解决此问题:
创建一个真正的类,它是一个TestDouble,并模拟您期望从真实API获得的结果。这实际上非常简单:您创建一个与普通MyApiClientTestDouble
具有相同签名的新MyApiClient
,只需更改所需的方法主体。
在你的service.yml中,你可能有这个:
parameters:
myApiClientClass: Namespace\Of\MyApiClient
service:
myApiClient:
class: %myApiClientClass%
如果是这种情况,您可以通过在config_test.yml中添加以下内容来轻松覆盖所采用的类:
parameters:
myApiClientClass: Namespace\Of\MyApiClientTestDouble
现在,服务容器将在测试时使用您的TestDouble。如果两个类具有相同的签名,则不需要其他任何内容。我不知道它是否或如何与DI Extras Bundle一起使用。但我想有办法。
或者你可以创建一个ApiDouble,实现一个“真正的”API,其行为方式与外部API相同但返回测试数据。然后,您将通过服务容器处理API的URI(例如,setter注入)并创建一个指向正确API的参数变量(在开发或测试的情况下为测试版,在生产环境的情况下为真实的API) )。
第三种方式有点hacky,但你总是可以在你的测试request
中创建一个私有方法,它首先以正确的方式设置容器,然后调用客户端发出请求。
答案 3 :(得分:2)
我不知道你是否曾经发现如何解决你的问题。但这是我使用的解决方案。这对其他发现这一点的人也有好处。
经过长时间搜索多个客户端请求之间模拟服务的问题后,我找到了这篇博文:
http://blog.lyrixx.info/2013/04/12/symfony2-how-to-mock-services-during-functional-tests.html
lyrixx讨论当你尝试发出另一个请求时,每次请求后内核如何关闭,使服务覆盖无效。
为了解决这个问题,他创建了一个仅用于功能测试的AppTestKernel。
这个AppTestKernel扩展了AppKernel,只应用了一些处理程序来修改内核: lyrixx blogpost的代码示例。
<?php
// app/AppTestKernel.php
require_once __DIR__.'/AppKernel.php';
class AppTestKernel extends AppKernel
{
private $kernelModifier = null;
public function boot()
{
parent::boot();
if ($kernelModifier = $this->kernelModifier) {
$kernelModifier($this);
$this->kernelModifier = null;
};
}
public function setKernelModifier(\Closure $kernelModifier)
{
$this->kernelModifier = $kernelModifier;
// We force the kernel to shutdown to be sure the next request will boot it
$this->shutdown();
}
}
当您需要覆盖测试中的服务时,请在testAppKernel上调用setter并应用模拟
class TwitterTest extends WebTestCase
{
public function testTwitter()
{
$twitter = $this->getMock('Twitter');
// Configure your mock here.
static::$kernel->setKernelModifier(function($kernel) use ($twitter) {
$kernel->getContainer()->set('my_bundle.twitter', $twitter);
});
$this->client->request('GET', '/fetch/twitter'));
$this->assertSame(200, $this->client->getResponse()->getStatusCode());
}
}
在遵循本指南后,我遇到了一些问题,让phpunittest启动时使用新的AppTestKernel。
我发现symfonys WebTestCase(https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php) 获取它找到的第一个AppKernel文件。因此,摆脱这种情况的一种方法是将AppTestKernel上的名称更改为在AppKernel之前或覆盖取代TestKernel的方法
这里我重写WebTestCase中的getKernelClass以查找* TestKernel.php
protected static function getKernelClass()
{
$dir = isset($_SERVER['KERNEL_DIR']) ? $_SERVER['KERNEL_DIR'] : static::getPhpUnitXmlDir();
$finder = new Finder();
$finder->name('*TestKernel.php')->depth(0)->in($dir);
$results = iterator_to_array($finder);
if (!count($results)) {
throw new \RuntimeException('Either set KERNEL_DIR in your phpunit.xml according to http://symfony.com/doc/current/book/testing.html#your-first-functional-test or override the WebTestCase::createKernel() method.');
}
$file = current($results);
$class = $file->getBasename('.php');
require_once $file;
return $class;
}
在此之后,您的测试将加载新的AppTestKernel,您将能够在多个客户端请求之间模拟服务。
答案 4 :(得分:2)
根据Mibsen的回答,你也可以通过扩展WebTestCase并覆盖createClient方法以类似的方式设置它。这些方面的东西:
class MyTestCase extends WebTestCase
{
private static $kernelModifier = null;
/**
* Set a Closure to modify the Kernel
*/
public function setKernelModifier(\Closure $kernelModifier)
{
self::$kernelModifier = $kernelModifier;
$this->ensureKernelShutdown();
}
/**
* Override the createClient method in WebTestCase to invoke the kernelModifier
*/
protected static function createClient(array $options = [], array $server = [])
{
static::bootKernel($options);
if ($kernelModifier = self::$kernelModifier) {
$kernelModifier->__invoke();
self::$kernelModifier = null;
};
$client = static::$kernel->getContainer()->get('test.client');
$client->setServerParameters($server);
return $client;
}
}
然后在测试中你会做类似的事情:
class ApplicationControllerTest extends MyTestCase
{
public function testSomething()
{
$apiClient = $this->getMockMyApiClient();
$this->setKernelModifier(function () use ($apiClient) {
static::$kernel->getContainer()->set('myapiclient', $apiClient);
});
$client = static::createClient();
.....
答案 5 :(得分:0)
制作模拟:
$mock = $this->getMockBuilder($className)
->disableOriginalConstructor()
->getMock();
$mock->method($method)->willReturn($return);
替换mock-object上的service_name:
$client = static::createClient()
$client->getContainer()->set('service_name', $mock);
我的问题是使用:
self::$kernel->getContainer();
答案 6 :(得分:0)
我在Symfony 4.4中也遇到了同样的问题。
阅读后
Create mocks in api functional testing with Symfony
我找到了解决方法-self::ensureKernelShutdown()
...
$client->request('GET', '/any/url/');
$this->assertEquals('stdClass', get_class($client->getContainer()->get($keyName))); // Passes.
self::ensureKernelShutdown()
$client->request('GET', '/any/url/');
$this->assertEquals('stdClass', get_class($client->getContainer()->get($keyName))); // Passes.
...