symfonytestslistener - Symfony 2 pruebas funcionales con servicios simulados.
symfony test querybuilder (6)
Cuando llamas a self::createClient()
, obtienes una instancia de arranque del kernel de Symfony2. Eso significa que, toda la configuración es analizada y cargada. Cuando envías una solicitud, dejas que el sistema haga su trabajo por primera vez, ¿verdad?
Después de la primera solicitud, es posible que desee verificar qué sucedió y, por lo tanto, el núcleo está en un estado en el que se envía la solicitud, pero aún se está ejecutando.
Si ahora ejecuta una segunda solicitud, la arquitectura web requiere que el kernel se reinicie, porque ya ejecutó una solicitud. Este reinicio, en su código, se ejecuta cuando ejecuta una solicitud por segunda vez.
Si desea iniciar el kernel y modificarlo antes de que se le envíe la solicitud (como usted desea), debe cerrar la instancia del kernel anterior e iniciar una nueva.
Puedes hacerlo simplemente ejecutando self::createClient()
. Ahora nuevamente tienes que aplicar tu simulacro, como hiciste la primera vez.
Este es el código modificado de su segundo ejemplo:
public function testAMockServiceCanBeAccessedByMultipleRequests()
{
$keyName = ''testing123'';
$client = static::createClient();
$client->getContainer()->set($keyName, new /stdClass());
// Check our object is still set on the container.
$this->assertEquals(''stdClass'', get_class($client->getContainer()->get($keyName)));
$client->request(''GET'', ''/any/url/'');
$this->assertEquals(''stdClass'', get_class($client->getContainer()->get($keyName)));
# addded these two lines here:
$client = static::createClient();
$client->getContainer()->set($keyName, new /stdClass());
$client->request(''GET'', ''/any/url/'');
$this->assertEquals(''stdClass'', get_class($client->getContainer()->get($keyName)));
}
Ahora es posible que desee crear un método separado, que se burle de la nueva instancia para usted, por lo que no tiene que copiar su código ...
Tengo un controlador para el que me gustaría crear pruebas funcionales. Este controlador realiza solicitudes HTTP a una API externa a través de una clase MyApiClient
. Necesito burlarme de esta clase MyApiClient
, para poder probar cómo responde mi controlador para respuestas dadas (por ejemplo, qué hará si la clase MyApiClient
devuelve una respuesta 500).
No tengo problemas para crear una versión MyApiClient
clase MyApiClient
mediante el estándar PHPUnit mockbuilder: el problema que tengo es hacer que mi controlador use este objeto para más de una solicitud.
Actualmente estoy haciendo lo siguiente en mi prueba:
class ApplicationControllerTest extends WebTestCase
{
public function testSomething()
{
$client = static::createClient();
$apiClient = $this->getMockMyApiClient();
$client->getContainer()->set(''myapiclient'', $apiClient);
$client->request(''GET'', ''/my/url/here'');
// Some assertions: Mocked API client returns 500 as expected.
$client->request(''GET'', ''/my/url/here'');
// Some assertions: Mocked API client is not used: Actual MyApiClient instance is being used instead.
}
protected function getMockMyApiClient()
{
$client = $this->getMockBuilder(''Namespace/Of/MyApiClient'')
->setMethods(array(''doSomething''))
->getMock();
$client->expects($this->any())
->method(''doSomething'')
->will($this->returnValue(500));
return $apiClient;
}
}
Parece que el contenedor se está reconstruyendo cuando se realiza la segunda solicitud, lo que hace que se MyApiClient
a crear una instancia de MyApiClient
. La clase MyApiClient
está configurada para ser un servicio a través de una anotación (utilizando el paquete adicional JMS DI) y se inyecta en una propiedad del controlador a través de una anotación.
Si pudiera, dividiría cada solicitud en su propia prueba para evitar hacer esto, pero desafortunadamente no puedo: necesito hacer una solicitud al controlador a través de una acción GET y luego POSTAR el formulario que trae. Me gustaría hacer esto por dos razones:
1) El formulario utiliza la protección CSRF, por lo tanto, si simplemente envío POST directamente al formulario sin usar el rastreador para enviarlo, el formulario falla en la verificación CSRF.
2) La prueba de que el formulario genera la solicitud POST correcta cuando se envía es una bonificación.
¿Alguien tiene alguna sugerencia sobre cómo hacer esto?
EDITAR:
Esto se puede expresar en la siguiente prueba de unidad que no depende de ninguno de mi código, por lo que puede ser más claro:
public function testAMockServiceCanBeAccessedByMultipleRequests()
{
$client = static::createClient();
// Set the container to contain an instance of stdClass at key ''testing123''.
$keyName = ''testing123'';
$client->getContainer()->set($keyName, new /stdClass());
// Check our object is still set on the container.
$this->assertEquals(''stdClass'', get_class($client->getContainer()->get($keyName))); // Passes.
$client->request(''GET'', ''/any/url/'');
$this->assertEquals(''stdClass'', get_class($client->getContainer()->get($keyName))); // Passes.
$client->request(''GET'', ''/any/url/'');
$this->assertEquals(''stdClass'', get_class($client->getContainer()->get($keyName))); // Fails.
}
Esta prueba falla, incluso si llamo a $client->getContainer()->set($keyName, new /stdClass());
Inmediatamente antes de la segunda llamada a request()
De acuerdo con la respuesta de Mibsen, también puede configurar esto de una manera similar al extender WebTestCase y anular el método createClient. Algo a lo largo de estas líneas:
class MyTestCase extends WebTestCase
{
private static $kernelModifier = null;
/**
* Set a Closure to modify the Kernel
*/
public function setKernelModifier(/Closure $kernelModifier)
{
self::$kernelModifier = $kernelModifier;
$this->ensureKernelShutdown();
}
/**
* Override the createClient method in WebTestCase to invoke the kernelModifier
*/
protected static function createClient(array $options = [], array $server = [])
{
static::bootKernel($options);
if ($kernelModifier = self::$kernelModifier) {
$kernelModifier->__invoke();
self::$kernelModifier = null;
};
$client = static::$kernel->getContainer()->get(''test.client'');
$client->setServerParameters($server);
return $client;
}
}
Luego en la prueba harías algo como:
class ApplicationControllerTest extends MyTestCase
{
public function testSomething()
{
$apiClient = $this->getMockMyApiClient();
$this->setKernelModifier(function () use ($apiClient) {
static::$kernel->getContainer()->set(''myapiclient'', $apiClient);
});
$client = static::createClient();
.....
El comportamiento que está experimentando es en realidad lo que experimentaría en cualquier escenario real, ya que PHP no comparte nada y reconstruye toda la pila en cada solicitud. El conjunto de pruebas funcionales imita este comportamiento para no generar resultados erróneos. Un ejemplo sería la doctrina, que tiene un ObjectCache, por lo que podría crear objetos, no guardarlos en la base de datos y todas sus pruebas pasarían, ya que elimina los objetos de la caché todo el tiempo.
Puedes resolver este problema de diferentes maneras:
Cree una clase real que sea un TestDouble y emule los resultados que esperaría de la API real. En realidad, esto es muy fácil: crea un nuevo MyApiClientTestDouble
con la misma firma que su MyApiClient
normal, y solo cambia los cuerpos de los métodos donde sea necesario.
En tu service.yml, puedes tener esto:
parameters:
myApiClientClass: Namespace/Of/MyApiClient
service:
myApiClient:
class: %myApiClientClass%
Si este es el caso, puede sobrescribir fácilmente qué clase se toma agregando lo siguiente a su config_test.yml:
parameters:
myApiClientClass: Namespace/Of/MyApiClientTestDouble
Ahora el contenedor de servicio utilizará su TestDouble cuando realice la prueba. Si ambas clases tienen la misma firma, no se necesita nada más. No sé si o cómo funciona esto con el paquete DI Extras. Pero supongo que hay una manera.
O puede crear un ApiDouble, implementando una API "real" que se comporte de la misma manera que su API externa, pero devuelve datos de prueba. Luego, haría que el URI de su API sea manejado por el contenedor de servicios (por ejemplo, la inyección del definidor) y crearía una variable de parámetros que apunte a la API correcta (la prueba en caso de desarrollo o prueba y la real en el entorno de producción). ).
La tercera forma es un poco pirateada, pero siempre puede crear un método privado dentro de su request
pruebas, que primero configura el contenedor de la manera correcta y luego llama al cliente para realizar la solicitud.
Hacer una burla
$mock = $this->getMockBuilder($className)
->disableOriginalConstructor()
->getMock();
$mock->method($method)->willReturn($return);
Reemplace service_name en mock-object:
$client = static::createClient()
$client->getContainer()->set(''service_name'', $mock);
Mi problema fue usar:
self::$kernel->getContainer();
No sé si alguna vez se enteró de cómo solucionar su problema. Pero aquí está la solución que utilicé. Esto también es bueno para otras personas que encuentran esto.
Después de una larga búsqueda del problema con la burla de un servicio entre varias solicitudes de clientes, encontré esta publicación de blog:
http://blog.lyrixx.info/2013/04/12/symfony2-how-to-mock-services-during-functional-tests.html
lyrixx habla sobre cómo el núcleo se apaga después de cada solicitud, lo que hace que el servicio se invalide cuando intente realizar otra solicitud.
Para solucionar esto, crea un AppTestKernel utilizado solo para las pruebas de función.
Este AppTestKernel amplía el AppKernel y solo aplica algunos manejadores para modificar el Kernel: Ejemplos de código de lyrixx blogpost.
<?php
// app/AppTestKernel.php
require_once __DIR__.''/AppKernel.php'';
class AppTestKernel extends AppKernel
{
private $kernelModifier = null;
public function boot()
{
parent::boot();
if ($kernelModifier = $this->kernelModifier) {
$kernelModifier($this);
$this->kernelModifier = null;
};
}
public function setKernelModifier(/Closure $kernelModifier)
{
$this->kernelModifier = $kernelModifier;
// We force the kernel to shutdown to be sure the next request will boot it
$this->shutdown();
}
}
Cuando luego necesita anular un servicio en su prueba, llama al configurador en el testAppKernel y aplica el simulacro
class TwitterTest extends WebTestCase
{
public function testTwitter()
{
$twitter = $this->getMock(''Twitter'');
// Configure your mock here.
static::$kernel->setKernelModifier(function($kernel) use ($twitter) {
$kernel->getContainer()->set(''my_bundle.twitter'', $twitter);
});
$this->client->request(''GET'', ''/fetch/twitter''));
$this->assertSame(200, $this->client->getResponse()->getStatusCode());
}
}
Después de seguir esta guía, tuve algunos problemas para iniciar el phpunittest con el nuevo AppTestKernel.
Me enteré de que Symfonys WebTestCase ( https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php ) toma el primer archivo de AppKernel que encuentra. Así que una forma de salir de esto es cambiar el nombre en el AppTestKernel para que aparezca antes de AppKernel o anular el método para tomar el TestKernel en su lugar
Aquí anulo el getKernelClass en el WebTestCase para buscar un * TestKernel.php
protected static function getKernelClass()
{
$dir = isset($_SERVER[''KERNEL_DIR'']) ? $_SERVER[''KERNEL_DIR''] : static::getPhpUnitXmlDir();
$finder = new Finder();
$finder->name(''*TestKernel.php'')->depth(0)->in($dir);
$results = iterator_to_array($finder);
if (!count($results)) {
throw new /RuntimeException(''Either set KERNEL_DIR in your phpunit.xml according to http://symfony.com/doc/current/book/testing.html#your-first-functional-test or override the WebTestCase::createKernel() method.'');
}
$file = current($results);
$class = $file->getBasename(''.php'');
require_once $file;
return $class;
}
Después de esto, sus pruebas se cargarán con el nuevo AppTestKernel y podrá simular servicios entre varias solicitudes de clientes.
Pensé que me gustaría saltar aquí. Chrisc, creo que lo que quieres está aquí:
https://github.com/PolishSymfonyCommunity/SymfonyMockerContainer
Estoy de acuerdo con su enfoque general, configurar esto en el contenedor de servicios como parámetro no es realmente un buen enfoque. La idea es poder simular esto dinámicamente durante las ejecuciones de prueba individuales.