Symfony 2 tests fonctionnels avec des services moqués

j'ai un contrôleur j'aimerais réaliser des tests fonctionnels pour. Ce controller effectue des requêtes HTTP vers une API externe via un MyApiClient classe. - Je besoin pour se moquer de cette MyApiClient class, de sorte que je puisse tester comment mon controller répond pour des réponses données (par exemple, qu'est-ce qu'il va faire si le MyApiClient la classe renvoie une réponse de 500).

Je n'ai aucun problème à créer une version moquée du MyApiClient class via le standard PHPUnit mockbuilder: le problème que j'ai est d'obtenir mon contrôleur pour utilisez cet objet pour plus d'une demande.

je suis actuellement en train de faire la suite de mon test:

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('NamespaceOfMyApiClient')
            ->setMethods(array('doSomething'))
            ->getMock();

        $client->expects($this->any())
            ->method('doSomething')
            ->will($this->returnValue(500));

        return $apiClient;
    }
}

il semble que le conteneur est en cours de reconstruction lorsque la seconde requête est faite, provoquant le MyApiClient pour être instancié à nouveau. MyApiClient class est configuré pour être un service via une annotation (en utilisant le pack supplémentaire JMS DI) et injecté dans une propriété du contrôleur via une annotation.

j'ai divisé chaque requête en son propre test pour travailler autour de faire cela si je le pouvais, mais malheureusement je ne peux pas: j'ai besoin de faire une requête au contrôleur via une action GET et ensuite poster le formulaire qu'il ramène. Je voudrais faire cela pour deux raisons:

1) le formulaire utilise la protection CSRF, donc si je poste directement sur le formulaire sans utiliser le crawler pour le soumettre, le formulaire échoue la vérification CSRF.

2) tester que le formulaire génère la requête POST correcte quand il est soumis est un bonus.

quelqu'un aurait-il des suggestions sur la façon de faire cela?

EDIT:

ceci peut être exprimé dans le test unitaire suivant qui ne dépend d'aucun de mes codes, donc peut être plus clair:

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.
}

ce test échoue, même si j'appelle $client->getContainer()->set($keyName, new stdClass()); immédiatement avant le deuxième appel à request()

21
demandé sur ChrisC 2013-03-11 18:52:53

6 réponses

j'ai pensé que je pourrais sauter ici. Chrisc, je pense que ce que vous voulez, c'est ici:

https://github.com/PolishSymfonyCommunity/SymfonyMockerContainer

je suis d'accord avec votre approche générale, configurer ceci dans le conteneur de service comme paramètre n'est vraiment pas une bonne approche. L'idée est d'être capable de se moquer dynamiquement de cela lors des essais individuels.

8
répondu genexp 2013-07-03 17:47:44

quand vous appelez self::createClient(), vous obtenez une instance démarrée du noyau Symfony2. Cela signifie que toute la configuration est analysée et chargée. Quand vous envoyez une requête, vous laissez le système faire son travail pour la première fois, non?

après la première requête, vous pouvez vérifier ce qui s'est passé, et par conséquent, le noyau est dans un état, où la requête est envoyée, mais il est toujours en cours d'exécution.

si vous lancez maintenant une deuxième requête, l'architecture web exige que le noyau redémarre, car il déjà présenté une demande. Ce redémarrage, dans votre code, est exécutée, lorsque vous exécutez une requête pour la deuxième fois.

si vous voulez démarrer le noyau et le modifier avant que la requête ne lui soit envoyée (comme vous le voulez), vous devez arrêter l'ancienne instance du noyau et en démarrer une nouvelle.

Vous pouvez le faire simplement en le relançant self::createClient(). Maintenant vous avez pour appliquer votre maquette, comme vous l'avez fait la première fois.

C'est le code modifié de votre deuxième exemple:

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)));
}

Maintenant vous pouvez vouloir créer une méthode séparée, qui se moque de l'instance fraîche pour vous, donc vous n'avez pas à copier votre code ...

8
répondu SimonSimCity 2013-11-13 10:29:18

le comportement que vous rencontrez est en fait ce que vous rencontreriez dans n'importe quel scénario réel, car PHP ne partage rien et reconstruit la pile entière sur chaque requête. La suite d'essais fonctionnelle imite ce comportement pour ne pas générer de mauvais résultats. Un exemple serait doctrine, qui a un ObjectCache, de sorte que vous pouvez créer des objets, pas les sauver à la base de données et vos tests seraient tous passer parce qu'il prend les objets hors du cache tout le temps.

Vous pouvez résoudre ce problème de différentes façons:

Créer une véritable classe qui est un TestDouble et émule les résultats que vous attendez de la vraie API. C'est en fait très simple: Vous créez un nouveau MyApiClientTestDouble avec la même signature que votre normal MyApiClient, et il suffit de changer les corps de la méthode si nécessaire.

à votre service.yml, vous allez bien ce pourrait être:

parameters:
  myApiClientClass: Namespace\Of\MyApiClient

service:
  myApiClient:
    class: %myApiClientClass%

si c'est le cas, vous pouvez facilement écraser quelle classe est prise en ajoutant ce qui suit à votre config_test.yml:

parameters:
  myApiClientClass: Namespace\Of\MyApiClientTestDouble

maintenant le conteneur de service va utiliser votre double TestDouble lors des tests. Si les deux classes ont la même signature, rien de plus n'est nécessaire. Je ne sais pas si et comment ça marche avec le paquet D'accessoires. mais je pense qu'il y a un moyen.

ou vous pouvez créer un ApiDouble, implémentant une API" réelle " qui se comporte de la même manière que votre API externe mais renvoie des données de test. Vous feriez alors L'URI de votre API manipulé par le conteneur de service (par exemple setter injection) et créer une variable de paramètres qui pointe vers la bonne API (la variable test dans le cas de dev ou test et la variable réelle dans le cas de l'environnement de production).

La troisième voie est très orthodoxe, mais vous pouvez toujours faire une méthode privée à l'intérieur de vos tests request qui d'abord configure le conteneur de la bonne façon et appelle ensuite le client pour faire la demande.

2
répondu Sgoettschkes 2013-03-11 22:05:39

je ne sais pas si vous avez déjà trouvé comment résoudre votre problème. Mais voici la solution que j'ai utilisée. C'est aussi bon pour les autres qui trouvent ça.

après une longue recherche pour le problème de moquerie d'un service entre plusieurs demandes de clients, j'ai trouvé ce billet de blog:

http://blog.lyrixx.info/2013/04/12/symfony2-how-to-mock-services-during-functional-tests.html

lyrixx parler de la fermeture du noyau après chaque requête le service outrepasse la validité lorsque vous essayez de faire une autre demande.

pour corriger cela, il crée un Applestkernel utilisé uniquement pour les tests de fonction.

cet appel étend L'appel et n'applique que quelques gestionnaires pour modifier le noyau: Exemples de Code du blog de lyrixx.

<?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();
    }
}

lorsque vous avez besoin de remplacer un service dans votre test, vous appelez le setter sur le testaments et appliquez le mock

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());
    }
}

Après avoir suivi ce guide j'ai eu quelques problèmes pour obtenir le phpunittest au démarrage avec le nouveau AppTestKernel.

j'ai découvert que le WebTestCase symfonys (https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php) Prend le premier fichier AppKernel qu'il trouve. Ainsi, une façon de s'en sortir est de changer le nom sur L'AppTestKernel à venir avant L'AppKernel ou d'outrepasser la méthode pour prendre le TestKernel à la place

ici i Outrepasser le getKernelClass dans la WebTestCase pour chercher 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;
}

après cela, vos tests se chargeront avec le nouvel AppTestKernel et vous pourrez simuler des services entre plusieurs demandes de clients.

2
répondu Mibsen 2013-10-31 11:24:29

basé sur la réponse de Mibsen vous pouvez aussi configurer cela de la même manière en étendant la WebTestCase et en remplaçant la méthode createClient. Quelque chose le long de ces lignes:

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;
    }
}

puis dans le test vous feriez quelque chose comme:

class ApplicationControllerTest extends MyTestCase
{
    public function testSomething()
    {
        $apiClient = $this->getMockMyApiClient();

        $this->setKernelModifier(function () use ($apiClient) {
            static::$kernel->getContainer()->set('myapiclient', $apiClient);
        });

        $client = static::createClient();

        .....
2
répondu Jeff Shillitto 2015-10-01 04:28:01

Faire un mock:

$mock = $this->getMockBuilder($className)
             ->disableOriginalConstructor()
             ->getMock();

$mock->method($method)->willReturn($return);

remplacer service_name sur mock-object:

$client = static::createClient()
$client->getContainer()->set('service_name', $mock);

Mon problème était d'utiliser:

self::$kernel->getContainer();
0
répondu Lebnik 2017-06-20 03:26:00