私はphpをモックしようとしていますfinal class
ですが、宣言されているのでfinal
このエラーを受け取り続けます:
PHPUnit_Framework_Exception: Class "Doctrine\ORM\Query" is declared "final" and cannot be mocked.
新しいフレームワークを導入せずに、単体テストだけでこのfinal
動作を回避する方法はありますか?
あなたは他のフレームワークを使いたくないと言ったので、あなたは1つのオプションだけを残しています: opz
uopzは、runkit-and-scary-stuffジャンルのブラックマジック拡張であり、QAインフラストラクチャの支援を目的としています。
opz_flags は、関数、メソッド、およびクラスのフラグを変更できる関数です。
<?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());
?>
収まる
bool(false)
この特定のdoctrineクエリ模擬回答を探している人への遅い応答。
「最終」宣言なのでDoctrine\ORM\Queryをモックすることはできませんが、Queryクラスのコードを見ると、その拡張AbstractQueryクラスがあり、それをモックする問題はないはずです。
/** @var \PHPUnit_Framework_MockObject_MockObject|AbstractQuery $queryMock */
$queryMock = $this
->getMockBuilder('Doctrine\ORM\AbstractQuery')
->disableOriginalConstructor()
->setMethods(['getResult'])
->getMockForAbstractClass();
このページで説明されているこの状況の回避策がある mockeryテストフレームワーク を確認することをお勧めします。 最終クラス/メソッドの扱い :
モックしたいインスタンス化されたオブジェクトを\ Mockery :: mock()に渡すことにより、プロキシモックを作成できます。つまり、Mockeryは実際のオブジェクトにプロキシを生成し、期待値を設定して満たすためにメソッド呼び出しを選択的にインターセプトします。
例として、これは次のようなことを許可します:
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());
}
私はあなたが何をする必要があるかわかりませんが、私はこの助けを願っています
小さなライブラリ Bypass Finals がまさにそのような目的のためにあります。 ブログ投稿 で詳細に説明されています。
クラスをロードする前に、このユーティリティを有効にするだけです。
DG\BypassFinals::enable();
最終クラスをモックしたいとき、それは 依存関係逆転原理 を利用する絶好の機会です:
コンクリートではなく、抽象化に依存するべきです。
モックとは、抽象化(インターフェースまたは抽象クラス)を作成し、それを最終クラスに割り当て、抽象化をモック化することを意味します。
PHPUnitを使用しているようです。 この回答からファイナルをバイパスする を使用できます。
セットアップはbootstrap.php
より少しだけです。 PHPUnitの最終クラスをモックする方法 の「なぜ」を読んでください。
こちらが「ハウ」↓
あなたはバイパス呼び出しでフックを使用する必要があります:
<?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.xml
:
<phpunit bootstrap="vendor/autoload.php">
<extensions>
<extension class="Hook\BypassFinalHook"/>
</extensions>
</phpunit>
次に、最終クラスをモックできます:
Doctrine\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
オブジェクトを返します。モックにはDoctrine\ORM\AbstractQuery
を使用できませんでした。setMaxResults
メソッドがなく、他のフレームワークを導入したくなかったためです。クラスのfinal
制限を克服するために、私は anonymous classes in PHP 7を使用します。これは非常に簡単に作成できます。テストケースクラスでは、持ってる:
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);
...
}
おかしい方法:)
PHP7.1、PHPUnit5.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;
}