Подтвердить что ты не робот

Функциональные тесты Symfony 2 с издеваемыми услугами

У меня есть контроллер, над которым я хотел бы создать функциональные тесты. Этот контроллер делает HTTP-запросы внешним API через класс MyApiClient. Мне нужно высмеять этот класс MyApiClient, поэтому я могу проверить, как мой контроллер отвечает за заданные ответы (например, что он будет делать, если класс MyApiClient возвращает ответ 500).

У меня нет проблем с созданием издевавшейся версии класса MyApiClient с помощью стандартного PHPUnit mockbuilder: проблема, с которой я столкнулась, заключается в том, что мой контроллер использует этот объект для более чем одного запроса.

В моем тесте я делаю следующее:

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

Кажется, что контейнер перестраивается, когда выполняется второй запрос, в результате чего MyApiClient создается снова. Класс MyApiClient настроен как услуга посредством аннотации (с использованием JMS DI Extra Bundle) и вводится в свойство контроллера посредством аннотации.

Я бы разделил каждый запрос на свой собственный тест, чтобы обойти это, если бы мог, но, к сожалению, я не могу: мне нужно сделать запрос к контроллеру с помощью действия GET, а затем POST, который он приносит назад. Я хотел бы сделать это по двум причинам:

1) Форма использует защиту CSRF, поэтому, если я просто отправлю POST непосредственно в форму без использования искателя для ее отправки, форма не будет проверяться CSRF.

2) Проверка того, что форма генерирует правильный запрос POST, когда он отправлен, является бонусом.

Есть ли у кого-нибудь предложения о том, как это сделать?

EDIT:

Это может быть выражено в следующем unit test, который не зависит от какого-либо из моего кода, поэтому может быть яснее:

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

Этот тест выходит из строя, даже если я вызываю $client->getContainer()->set($keyName, new \stdClass()); непосредственно перед вторым вызовом request()

4b9b3361

Ответ 1

Я думал, что приеду сюда. Chrisc, я думаю, что вы хотите здесь:

https://github.com/PolishSymfonyCommunity/SymfonyMockerContainer

Я согласен с вашим общим подходом, настраивая это в контейнере службы, поскольку параметр действительно не подходит. Вся идея состоит в том, чтобы иметь возможность имитировать это динамически во время отдельных тестовых прогонов.

Ответ 2

Когда вы вызываете self::createClient(), вы получаете загруженный экземпляр ядра Symfony2. Это означает, что все конфиги проанализированы и загружены. Когда вы отправляете запрос, вы позволяете системе выполнять эту работу в первый раз, верно?

После первого запроса вы можете проверить, что произошло, и, следовательно, ядро ​​находится в состоянии, куда отправляется запрос, но он все еще работает.

Если теперь выполняется второй запрос, веб-архитектура требует, чтобы ядро ​​перезагрузилось, поскольку оно уже выполнило запрос. Эта перезагрузка в вашем коде выполняется во время выполнения запроса во второй раз.

Если вы хотите загрузить ядро ​​и изменить его до того, как запрос будет отправлен на него (как вы хотите), вам нужно закрыть старый экземпляр ядра и загрузить новый.

Вы можете сделать это, просто перезапустив self::createClient(). Теперь вам снова нужно применить ваш макет, как вы делали в первый раз.

Это модифицированный код вашего второго примера:

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

Теперь вы можете создать отдельный метод, который издевается над новым экземпляром для вас, поэтому вам не нужно копировать код...

Ответ 3

Поведение, которое вы испытываете, - это то, что вы испытали бы в любом реальном сценарии, поскольку PHP ничего не использует и восстанавливает весь стек по каждому запросу. Функциональный набор тестов имитирует это поведение, чтобы не генерировать неверные результаты. Одним из примеров может быть доктрина, которая имеет ObjectCache, поэтому вы можете создавать объекты, а не сохранять их в базе данных, и все тесты будут проходить, поскольку они все время выводят объекты из кэша.

Вы можете решить эту проблему по-разному:

Создайте настоящий класс, который является TestDouble и эмулирует результаты, которые вы ожидаете от реального API. Это на самом деле очень просто: вы создаете новый MyApiClientTestDouble с той же подписью, что и ваш обычный MyApiClient, и просто меняете тела методов там, где это необходимо.

В вашем service.yml, у вас все получится:

parameters:
  myApiClientClass: Namespace\Of\MyApiClient

service:
  myApiClient:
    class: %myApiClientClass%

Если это так, вы можете легко перезаписать класс, добавив следующее в свой config_test.yml:

parameters:
  myApiClientClass: Namespace\Of\MyApiClientTestDouble

Теперь при тестировании контейнер службы будет использовать ваш TestDouble. Если оба класса имеют одну и ту же подпись, больше ничего не требуется. Я не знаю, работает ли или как это работает с пакетом DI Extras Bundle. но я думаю, что есть способ.

Или вы можете создать ApiDouble, реализуя "настоящий" API, который ведет себя так же, как ваш внешний API, но возвращает тестовые данные. Затем вы должны сделать URI вашего API обработанным контейнером службы (например, установкой установки) и создать переменную параметров, которая указывает на правый API (тестовый в случае dev или test и реальный в случае производственной среды).

Третий способ немного взломан, но вы всегда можете сделать закрытый метод внутри своих тестов request, который сначала правильно настраивает контейнер, а затем вызывает клиента для выполнения запроса.

Ответ 4

Я не знаю, узнали ли вы когда-нибудь, как исправить свою проблему. Но вот решение, которое я использовал. Это также хорошо для других людей, которые находят это.

После долгого поиска проблемы с издевательством между несколькими клиентскими запросами я нашел это сообщение в блоге:

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

lyrixx расскажет о том, как ядро ​​отключается после каждого запроса, делающего сервис overrid недопустимым при попытке сделать другой запрос.

Чтобы исправить это, он создает AppTestKernel, используемый только для тестирования функций.

Этот AppTestKernel расширяет AppKernel и применяет только некоторые обработчики для модификации ядра: Примеры кода из блога 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();
    }
}

Когда вам нужно переопределить службу в своем тесте, вы вызываете установщик на testAppKernel и применяете макет

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

После этого руководства у меня возникли проблемы с запуском phpunittest с новым AppTestKernel.

Я узнал, что symfonys WebTestCase (https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php) Принимает первый файл AppKernel, который он находит. Таким образом, один из способов избежать этого - изменить имя на AppTestKernel, которое должно появиться перед AppKernel, или переопределить метод вместо TestKernel.

Здесь я переопределяю getKernelClass в WebTestCase для поиска * 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;
}

После этого ваши тесты загрузятся с новым AppTestKernel, и вы сможете обманывать службы между несколькими клиентскими запросами.

Ответ 5

Основываясь на ответе Mibsen, вы также можете установить это аналогичным образом, расширив WebTestCase и переопределив метод createClient. Что-то в этом роде:

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

Тогда в тесте вы сделаете что-то вроде:

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

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

        $client = static::createClient();

        .....

Ответ 6

Сделайте фальшивку:

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

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

Заменить имя_сервера на mock-object:

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

Моя проблема заключалась в использовании:

self::$kernel->getContainer();