danhabib
Estoy intentando burlarme de un php final class
pero como se declara final
Sigo recibiendo este error:
PHPUnit_Framework_Exception: Class "Doctrine\ORM\Query" is declared "final" and cannot be mocked.
Hay alguna forma de evitar esto final
comportamiento solo para mis pruebas unitarias sin introducir nuevos marcos?
Como mencionó que no desea utilizar ningún otro marco, solo se deja una opción: uopz
uopz es una extensión de magia negra del género runkit-and-scary-stuff, destinado a ayudar con la infraestructura de control de calidad.
uopz_flags es una función que puede modificar las banderas de funciones, métodos y clases.
<?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());
?>
Rendirá
bool(false)
Respuesta tardía para alguien que está buscando esta respuesta simulada de consulta de doctrina específica.
No puede burlarse de Doctrine\ORM\Query debido a su declaración “final”, pero si observa el código de la clase Query, verá que es una extensión de la clase AbstractQuery y no debería haber ningún problema para burlarse de ella.
/** @var \PHPUnit_Framework_MockObject_MockObject|AbstractQuery $queryMock */
$queryMock = $this
->getMockBuilder('Doctrine\ORM\AbstractQuery')
->disableOriginalConstructor()
->setMethods(['getResult'])
->getMockForAbstractClass();
-
Esto funciona para una clase marcada como final que amplía un resumen o implementa una interfaz. Si la clase en sí se define como final, entonces tendrá que usar una de las otras rondas de trabajo.
– b01
27 de junio de 2017 a las 14:02
-
Mucho más limpio en comparación con todos los demás. +1
– BentCoder
30 de marzo de 2018 a las 11:42
-
Esta solución se ve bastante bien, pero estoy tratando de hacer que funcione con codeception, y encuentro este error cuando llamo a ‘getResult’: tratando de configurar el método “getResult” que no se puede configurar porque no existe, no ha sido especificado, es definitivo o es estático
–Geoff Maddock
27 de agosto de 2018 a las 19:51
-
Después de un poco más de excavación, ¡usar ‘ejecutar’ en lugar de ‘getResult’ permitió que esto funcionara!
–Geoff Maddock
27 ago 2018 a las 20:20
Te sugiero que eches un vistazo a la marco de prueba de burla que tienen una solución para esta situación descrita en la página: Manejo de clases/métodos finales:
Puede crear un simulacro de proxy pasando el objeto instanciado que desea simular a \Mockery::mock(), es decir, Mockery luego generará un Proxy para el objeto real e interceptará selectivamente las llamadas a métodos con el fin de establecer y cumplir las expectativas.
Como ejemplo, esto permite hacer algo como esto:
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());
}
no se que tienes que hacer pero espero que te sirva
-
Cuando la clase burlona tiene dependencias finales que tienen dependencias finales, se está complicando. Parece útil solo para clases finales que no tienen dependencias.
– fabpico
11 de febrero de 2019 a las 9:14
Milo
hay una pequeña biblioteca Omitir finales precisamente para tal fin. Descrito en detalle por entrada en el blog.
Lo único que tiene que hacer es habilitar esta utilidad antes de que se carguen las clases:
DG\BypassFinals::enable();
fabuloso
Cuando quieras simular una clase final, es un momento perfecto para hacer uso de Principio de inversión de dependencia:
Uno debería depender de abstracciones, no de concreciones.
Para la burla significa: Crear una abstracción (interfaz o clase abstracta) y asignarla a la clase final, y burlarse de la abstracción.
-
De acuerdo, pero si la clase final pertenece a una biblioteca de terceros, no puede simplemente editarla. Sin embargo, lo que descubrí: si crea una interfaz en su código, y la clase final simplemente pasa a implementarlo – es decir, tiene las mismas firmas de método pero no
implements
palabra clave, seguirá funcionando 🙂– pstryk
15 de mayo de 2019 a las 9:20
-
En este caso debes adaptarte. Cree una nueva interfaz en su proyecto que será su dependencia simulada en lugar de la dependencia de terceros. La implementación de la nueva interfaz luego inyectará la dependencia de terceros que no se puede burlar. La implementación no debe tener ninguna lógica, solo actuar como puerta de enlace a los métodos de clase de terceros, eso no vale la pena para la prueba unitaria.
– fabpico
15 de mayo de 2019 a las 9:59
-
Bueno, debo retractarme de lo que acabo de decir. Las pruebas están pasando, sí, pero solo porque inyecté lo que quería e hice afirmaciones que se basan en ello. Sin embargo, la aplicación ya no funciona debido a un error: el método debería devolver una instancia de mi interfaz, pero en su lugar solo obtuvo una instancia de dicha clase final. Así se ve el
implements
la palabra clave es necesaria. En esta situación, su solución propuesta parece ser la única forma sensata. Gracias 🙂– pstryk
15 de mayo de 2019 a las 12:02
Tomás Votruba
Respuesta de 2019 para PHPUnit
Veo que estás usando PHPUnit. Puede usar los finales de omisión de esta respuesta.
La configuración es un poco más que bootstrap.php
. Leer “por qué” en Cómo simular clases finales en PHPUnit.
Aquí está “cómo” ↓
2 pasos
Necesita usar Hook con llamada de derivación:
<?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();
}
}
Actualizar phpunit.xml
:
<phpunit bootstrap="vendor/autoload.php">
<extensions>
<extension class="Hook\BypassFinalHook"/>
</extensions>
</phpunit>
Entonces tú puede burlarse de cualquier clase final:
-
De acuerdo, pero si la clase final pertenece a una biblioteca de terceros, no puede simplemente editarla. Sin embargo, lo que descubrí: si crea una interfaz en su código, y la clase final simplemente pasa a implementarlo – es decir, tiene las mismas firmas de método pero no
implements
palabra clave, seguirá funcionando 🙂– pstryk
15 de mayo de 2019 a las 9:20
-
En este caso debes adaptarte. Cree una nueva interfaz en su proyecto que será su dependencia simulada en lugar de la dependencia de terceros. La implementación de la nueva interfaz luego inyectará la dependencia de terceros que no se puede burlar. La implementación no debe tener ninguna lógica, solo actuar como puerta de enlace a los métodos de clase de terceros, eso no vale la pena para la prueba unitaria.
– fabpico
15 de mayo de 2019 a las 9:59
-
Bueno, debo retractarme de lo que acabo de decir. Las pruebas están pasando, sí, pero solo porque inyecté lo que quería e hice afirmaciones que se basan en ello. Sin embargo, la aplicación ya no funciona debido a un error: el método debería devolver una instancia de mi interfaz, pero en su lugar solo obtuvo una instancia de dicha clase final. Así se ve el
implements
la palabra clave es necesaria. En esta situación, su solución propuesta parece ser la única forma sensata. Gracias 🙂– pstryk
15 de mayo de 2019 a las 12:02
Vadym
Manera graciosa 🙂
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;
Podría crear una copia de la clase FINAL que no es FINAL y burlarse de ella
–Bryant Frankford
25/08/2015 a las 20:35
@BryantFrankford gracias por la solución. Si bien esto funcionaría, idealmente preferiría evitar escribir una nueva clase para esta situación específica. ¿No estaría al tanto de una solución que escalaría un poco mejor? Si esto se convierte en una puerta a mi proyecto, implementaré la solución anterior
– DanHabib
25 de agosto de 2015 a las 20:47
Aparte de cambiar la clase original para que no sea definitiva, personalmente no tengo otra solución.
–Bryant Frankford
25 de agosto de 2015 a las 20:48
Considere probar la interfaz de profecía de phpunit: phpunit.de/manual/current/en/…. Es un poco diferente y no le importan las finales y demás.
– Cerad
25 de agosto de 2015 a las 22:17
¿Puedes escribir una prueba unitaria para hacer, como ejemplo?
– Mateo
13 de octubre de 2015 a las 5:09