Clase final de simulación de PHP

8 minutos de lectura

avatar de usuario
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?

  • 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

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

avatar de usuario
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();

avatar de usuario
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

avatar de usuario
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:

ingrese la descripción de la imagen aquí

  • 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

avatar de usuario
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;

¿Ha sido útil esta solución?