J'essaie de me moquer d'un php final class
mais puisqu'il est déclaré final
je continue à recevoir cette erreur:
PHPUnit_Framework_Exception: Class "Doctrine\ORM\Query" is declared "final" and cannot be mocked.
Y at-il un moyen de contourner ce comportement final
uniquement pour mes tests unitaires sans introduire de nouveaux frameworks?
Puisque vous avez mentionné que vous ne souhaitiez utiliser aucun autre cadre, vous ne laissiez qu'une option: uopz
uopz est une extension de la magie noire du genre runkit-and-scary-stuff, destinée à aider à l'infrastructure QA.
uopz_flags est une fonction qui peut modifier les drapeaux de fonctions, méthodes et classes.
<?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());
?>
Va céder
bool(false)
Réponse tardive pour quelqu'un qui recherche cette réponse fictive à une requête de doctrine spécifique.
Vous ne pouvez pas vous moquer de Doctrine\ORM\Query en raison de sa déclaration "finale", mais si vous examinez le code de la classe Query, vous verrez que sa classe AbstractQuery est étendue et qu'il ne devrait y avoir aucun problème à s'en moquer.
/** @var \PHPUnit_Framework_MockObject_MockObject|AbstractQuery $queryMock */
$queryMock = $this
->getMockBuilder('Doctrine\ORM\AbstractQuery')
->disableOriginalConstructor()
->setMethods(['getResult'])
->getMockForAbstractClass();
Je vous suggère de jeter un coup d'œil au framework de test de moquerie qui propose une solution de contournement à cette situation décrite dans la page: Traitement des classes/méthodes finales :
Vous pouvez créer une maquette de proxy en passant l'objet instancié par vous souhaite se moquer de\Mockery :: mock (), c’est-à-dire que Mockery générera alors un Proxy vers l'objet réel et intercepter sélectivement les appels de méthode pour les objectifs d'établissement et de répondre aux attentes.
A titre d'exemple, cela permet de faire quelque chose comme ceci:
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());
}
Je ne sais pas ce que vous devez faire mais j'espère cette aide
Je suis tombé sur le même problème avec Doctrine\ORM\Query
. J'avais besoin de tester le code suivant:
public function someFunction()
{
// EntityManager was injected in the class
$query = $this->entityManager
->createQuery('SELECT t FROM Test t')
->setMaxResults(1);
$result = $query->getOneOrNullResult();
...
}
createQuery
renvoie l'objet Doctrine\ORM\Query
. Je ne pouvais pas utiliser Doctrine\ORM\AbstractQuery
pour ma maquette car il n'avait pas de méthode setMaxResults
et je ne voulais pas introduire d'autres frameworks . Pour surmonter la restriction final
sur la classe que j'utilise classes anonymes dans PHP 7, qui sont très faciles à créer. Dans ma classe de test, j'ai:
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;
}
Puis dans mon test:
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);
...
}
Drôle de façon :)
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;
J'ai implémenté l'approche @Vadym et l'ai mise à jour. Maintenant je l'utilise pour tester avec succès!
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;
}
Il y a une petite bibliothèque Bypass Finals exactement à cette fin. Décrit en détail par blog post .
Vous devez seulement activer cet utilitaire avant le chargement des classes:
DG\BypassFinals::enable();
Lorsque vous voulez vous moquer d’une dernière classe, c’est le moment idéal pour utiliser Principe de l’inversion de dépendance :
On devrait dépendre d'abstractions, pas de concrétions.
Cela signifie: créer une abstraction (classe d’interface ou abstraite) et l’affecter à la classe finale, puis se moquer de cette abstraction.