PHP模拟最终类
我试图模拟一个phpPHP模拟最终类,php,unit-testing,doctrine-orm,mocking,phpunit,Php,Unit Testing,Doctrine Orm,Mocking,Phpunit,我试图模拟一个phpfinal类,但由于它被声明为final,我一直收到这个错误: PHPUnit\u框架\u异常:类“条令\ORM\Query”声明为“最终”且不能模拟。 在不引入任何新框架的情况下,有没有办法绕过我的单元测试的最终行为?我建议您看看页面中描述的解决这种情况的方法: 您可以通过将实例化对象传递给您来创建代理模拟 希望模拟为\mockry::mock(),即mockry将生成一个 代理到真实对象,并有选择地截获方法调用 设定和满足期望的目的 例如,本许可证允许执行以下操作: cl
final类
,但由于它被声明为final
,我一直收到这个错误:
PHPUnit\u框架\u异常:类“条令\ORM\Query”声明为“最终”且不能模拟。
在不引入任何新框架的情况下,有没有办法绕过我的单元测试的
最终行为?我建议您看看页面中描述的解决这种情况的方法:
您可以通过将实例化对象传递给您来创建代理模拟
希望模拟为\mockry::mock(),即mockry将生成一个
代理到真实对象,并有选择地截获方法调用
设定和满足期望的目的
例如,本许可证允许执行以下操作:
class MockFinalClassTest extends \PHPUnit_Framework_TestCase {
public function testMock()
{
$em = \Mockery::mock("Doctrine\ORM\EntityManager");
$query = new Doctrine\ORM\Query($em);
$proxy = \Mockery::mock($query);
$this->assertNotNull($proxy);
$proxy->setMaxResults(4);
$this->assertEquals(4, $query->getMaxResults());
}
我不知道您需要做什么,但我希望这能帮助您因为您提到您不想使用任何其他框架,所以您只留下一个选项:
uopz是runkit和Thrable stuff流派的一个黑魔法扩展,旨在帮助QA基础设施
是一个可以修改函数、方法和类的标志的函数
<?php
final class Test {}
/** ZEND_ACC_CLASS is defined as 0, just looks nicer ... **/
uopz_flags(Test::class, null, ZEND_ACC_CLASS);
$reflector = new ReflectionClass(Test::class);
var_dump($reflector->isFinal());
?>
有趣的方式:)
PHP7.1,PHP5.7
<?php
use Doctrine\ORM\Query;
//...
$originalQuery = new Query($em);
$allOriginalMethods = get_class_methods($originalQuery);
// some "unmockable" methods will be skipped
$skipMethods = [
'__construct',
'staticProxyConstructor',
'__get',
'__set',
'__isset',
'__unset',
'__clone',
'__sleep',
'__wakeup',
'setProxyInitializer',
'getProxyInitializer',
'initializeProxy',
'isProxyInitialized',
'getWrappedValueHolderValue',
'create',
];
// list of all methods of Query object
$originalMethods = [];
foreach ($allOriginalMethods as $method) {
if (!in_array($method, $skipMethods)) {
$originalMethods[] = $method;
}
}
// Very dummy mock
$queryMock = $this
->getMockBuilder(\stdClass::class)
->setMethods($originalMethods)
->getMock()
;
foreach ($originalMethods as $method) {
// skip "unmockable"
if (in_array($method, $skipMethods)) {
continue;
}
// mock methods you need to be mocked
if ('getResult' == $method) {
$queryMock->expects($this->any())
->method($method)
->will($this->returnCallback(
function (...$args) {
return [];
}
)
);
continue;
}
// make proxy call to rest of the methods
$queryMock->expects($this->any())
->method($method)
->will($this->returnCallback(
function (...$args) use ($originalQuery, $method, $queryMock) {
$ret = call_user_func_array([$originalQuery, $method], $args);
// mocking "return $this;" from inside $originalQuery
if (is_object($ret) && get_class($ret) == get_class($originalQuery)) {
if (spl_object_hash($originalQuery) == spl_object_hash($ret)) {
return $queryMock;
}
throw new \Exception(
sprintf(
'Object [%s] of class [%s] returned clone of itself from method [%s]. Not supported.',
spl_object_hash($originalQuery),
get_class($originalQuery),
$method
)
);
}
return $ret;
}
))
;
}
return $queryMock;
我已经实现并更新了@Vadym方法。现在我用它成功地进行了测试
protected function getFinalMock($originalObject)
{
if (gettype($originalObject) !== 'object') {
throw new \Exception('Argument must be an object');
}
$allOriginalMethods = get_class_methods($originalObject);
// some "unmockable" methods will be skipped
$skipMethods = [
'__construct',
'staticProxyConstructor',
'__get',
'__set',
'__isset',
'__unset',
'__clone',
'__sleep',
'__wakeup',
'setProxyInitializer',
'getProxyInitializer',
'initializeProxy',
'isProxyInitialized',
'getWrappedValueHolderValue',
'create',
];
// list of all methods of Query object
$originalMethods = [];
foreach ($allOriginalMethods as $method) {
if (!in_array($method, $skipMethods)) {
$originalMethods[] = $method;
}
}
$reflection = new \ReflectionClass($originalObject);
$parentClass = $reflection->getParentClass()->name;
// Very dummy mock
$mock = $this
->getMockBuilder($parentClass)
->disableOriginalConstructor()
->setMethods($originalMethods)
->getMock();
foreach ($originalMethods as $method) {
// skip "unmockable"
if (in_array($method, $skipMethods)) {
continue;
}
// make proxy call to rest of the methods
$mock
->expects($this->any())
->method($method)
->will($this->returnCallback(
function (...$args) use ($originalObject, $method, $mock) {
$ret = call_user_func_array([$originalObject, $method], $args);
// mocking "return $this;" from inside $originalQuery
if (is_object($ret) && get_class($ret) == get_class($originalObject)) {
if (spl_object_hash($originalObject) == spl_object_hash($ret)) {
return $mock;
}
throw new \Exception(
sprintf(
'Object [%s] of class [%s] returned clone of itself from method [%s]. Not supported.',
spl_object_hash($originalObject),
get_class($originalObject),
$method
)
);
}
return $ret;
}
));
}
return $mock;
}
对于正在寻找这个特定答案的人,反应迟钝
您不能模拟Doctrine\ORM\Query,因为它是“final”声明,但如果您查看查询类代码,您将看到它扩展了AbstractQuery类,模拟它应该不会有任何问题
/** @var \PHPUnit_Framework_MockObject_MockObject|AbstractQuery $queryMock */
$queryMock = $this
->getMockBuilder('Doctrine\ORM\AbstractQuery')
->disableOriginalConstructor()
->setMethods(['getResult'])
->getMockForAbstractClass();
我在条令\ORM\Query
中偶然发现了同样的问题。我需要对以下代码进行单元测试:
public function someFunction()
{
// EntityManager was injected in the class
$query = $this->entityManager
->createQuery('SELECT t FROM Test t')
->setMaxResults(1);
$result = $query->getOneOrNullResult();
...
}
createQuery
返回Doctrine\ORM\Query
对象。我不能将doctor\ORM\AbstractQuery
用于我的mock,因为它没有setMaxResults
方法,而且我不想引入任何其他框架。
为了克服我在PHP7中使用的类的final
限制,这些类非常容易创建。在我的测试用例类中,我有:
private function getMockDoctrineQuery($result)
{
$query = new class($result) extends AbstractQuery {
private $result;
/**
* Overriding original constructor.
*/
public function __construct($result)
{
$this->result = $result;
}
/**
* Overriding setMaxResults
*/
public function setMaxResults($maxResults)
{
return $this;
}
/**
* Overriding getOneOrNullResult
*/
public function getOneOrNullResult($hydrationMode = null)
{
return $this->result;
}
/**
* Defining blank abstract method to fulfill AbstractQuery
*/
public function getSQL(){}
/**
* Defining blank abstract method to fulfill AbstractQuery
*/
protected function _doExecute(){}
};
return $query;
}
然后在我的测试中:
public function testSomeFunction()
{
// Mocking doctrine Query object
$result = new \stdClass;
$mockQuery = $this->getMockQuery($result);
// Mocking EntityManager
$entityManager = $this->getMockBuilder(EntityManagerInterface::class)->getMock();
$entityManager->method('createQuery')->willReturn($mockQuery);
...
}
有一个小图书馆正是为了这个目的。详细描述由
您只需在加载类之前启用此实用程序:
DG\BypassFinals::enable();
当你想模拟最后一节课时,这是一个利用以下机会的绝佳时机:
一个人应该依靠抽象,而不是具体
对于模拟,它意味着:创建一个抽象(接口或抽象类),并将其分配给最终的类,然后模拟抽象。2019 PHPUnit的答案
我看到你在用PHPUnit。你可以用
设置只是比bootstrap.php略多一些。请阅读中的“为什么”
这里是“如何”↓
2步
您需要将钩子与旁路调用一起使用:
<?php declare(strict_types=1);
use DG\BypassFinals;
use PHPUnit\Runner\BeforeTestHook;
final class BypassFinalHook implements BeforeTestHook
{
public function executeBeforeTest(string $test): void
{
BypassFinals::enable();
}
}
然后你可以模拟任何最后一节课:
您可以创建一个不是FINAL和mock的FINAL类的副本it@BryantFrankford谢谢你的解决方案。虽然这会起作用,但理想情况下,我宁愿避免为这种特定情况编写新类。你不会碰巧意识到一个可以更好地扩展的解决方案吗?如果这成为我的项目的门控,那么我将实现以上的解决方案,而不是改变原来的类不是最终的,我个人没有任何其他的解决方案。考虑尝试PHPUngy的预言界面:它有点不同,不关心期末考试等等。例如,你能写一个单元测试吗?这适用于标记为期末考试的类,它扩展了一个抽象或实现了一个接口。如果类本身被定义为final,那么您将不得不使用其他工作循环中的一个。与所有其他工作循环相比,它要干净得多+1此解决方案看起来相当不错,但我正在尝试使其与codeception配合使用,并在调用“getResult”时发现此错误:尝试配置方法“getResult”,该方法无法配置,因为它不存在,也未指定,是最终的,或者,在进一步挖掘之后,使用“execute”而不是“getResult”可以让它工作吗!PHP5.5还有其他选择吗?该工具本身现在可以与PHP5.4一起使用。您可以下载BypassFinances.php
,这是唯一需要的文件,并将其包括在内。这里有一篇更深入的文章:当mocking类具有最终依赖项时,它会变得一团糟。它似乎只对没有依赖项的最终类有用。好的,但是如果最终类属于第三方库,您不能简单地编辑它。然而,我发现:如果在代码中创建了一个接口,而最后一个类恰好实现了它——也就是说,它有相同的方法签名,但没有implements
关键字,它仍然可以工作:)在这种情况下,您应该进行调整。在项目中创建一个新接口,该接口将成为可模仿的依赖项,而不是第三方依赖项。然后,新接口的实现将注入不可修改的第三方依赖项。实现不应该有任何逻辑,只作为第三方类方法的网关,这不值得进行单元测试。测试通过了,是的,但仅仅是因为我注入了我想要的,并做出了依赖于它的断言。然而,由于错误,应用程序不再工作了:方法应该返回我接口的实例,但它只得到了最后一个类的实例。因此,看来implements
关键字是必要的。在这种情况下,您提出的解决方案似乎是唯一明智的方法。谢谢:)DG\code>stream\u wrapper\u register
。这可能会阻止依赖于相同机制的工具(f.e.感染)正常工作。
<?php declare(strict_types=1);
use DG\BypassFinals;
use PHPUnit\Runner\BeforeTestHook;
final class BypassFinalHook implements BeforeTestHook
{
public function executeBeforeTest(string $test): void
{
BypassFinals::enable();
}
}
<phpunit bootstrap="vendor/autoload.php">
<extensions>
<extension class="Hook\BypassFinalHook"/>
</extensions>
</phpunit>