web-dev-qa-db-ja.com

模擬サービスを使用したSymfony2の機能テスト

機能テストを作成したいコントローラーがあります。このコントローラーは、MyApiClientクラスを介して外部APIにHTTPリクエストを送信します。このMyApiClientクラスをモックアウトする必要があるので、特定の応答に対してコントローラーがどのように応答するかをテストできます(たとえば、MyApiClientクラスが500応答を返した場合はどうなりますか)。

標準のPHPUnitmockbuilderを使用してMyApiClientクラスのモックバージョンを作成することに問題はありません。私が抱えている問題は、コントローラーがこのオブジェクトを複数のリクエストに使用できるようにすることです。

私は現在、テストで次のことを行っています。

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

2番目のリクエストが行われたときにコンテナが再構築されているようで、MyApiClientが再度インスタンス化されます。 MyApiClientクラスは、アノテーションを介してサービスとして構成され(JMS DI Extra Bundleを使用)、アノテーションを介してコントローラーのプロパティに挿入されます。

可能であれば、これを回避するために各リクエストを独自のテストに分割しますが、残念ながらできません。GETアクションを介してコントローラーにリクエストを送信してから、POSTを実行する必要があります。それがもたらす形。私は2つの理由でこれをしたいと思います:

1)フォームはCSRF保護を使用しているため、クローラーを使用せずにフォームに直接POSTするだけで、フォームはCSRFチェックに失敗します。

2)フォームが送信されたときに正しいPOSTリクエストを生成することをテストすることはボーナスです。

誰かがこれを行う方法について何か提案がありますか?

編集:

これは、私のコードのいずれにも依存しない次の単体テストで表現できるため、より明確になる可能性があります。

_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());への2回目の呼び出しの直前にrequest()を呼び出しても、このテストは失敗します。

21
ChrisC

ここに飛び込むと思った。クリス、あなたが欲しいものはここにあると思います:

https://github.com/PolishSymfonyCommunity/SymfonyMockerContainer

私はあなたの一般的なアプローチに同意します。これをサービスコンテナでパラメータとして設定することは、実際には良いアプローチではありません。全体的なアイデアは、個々のテスト実行中にこれを動的にモックできるようにすることです。

8
genexp

self::createClient()を呼び出すと、Symfony2カーネルの起動インスタンスを取得します。つまり、すべての構成が解析されてロードされます。リクエストを送信するときに、システムに初めてその仕事をさせますよね?

最初のリクエストの後、何が起こったのかを確認したい場合があります。そのため、カーネルはリクエストが送信された状態ですが、まだ実行中です。

ここで2番目のリクエストを実行する場合、Webアーキテクチャでは、すでにリクエストを実行しているため、カーネルを再起動する必要があります。この再起動は、コードで、2回目のリクエストの実行時に実行されます。

要求が送信される前にカーネルを起動して変更する場合(必要に応じて)、古いカーネルインスタンスをシャットダウンして新しいカーネルインスタンスを起動する必要があります。

self::createClient()を再実行するだけでそれを行うことができます。今度は、最初に行ったように、モックを再度適用する必要があります。

これは、2番目の例の変更されたコードです。

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

ここで、新しいインスタンスをモックする別のメソッドを作成することをお勧めします。そのため、コードをコピーする必要はありません...

8
SimonSimCity

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

        .....
2
Jeff S.

PHPは何も共有せず、リクエストごとにスタック全体を再構築するため、実際に発生している動作は、実際のシナリオで発生する動作です。機能テストスイートは、この動作を模倣して、間違ったものを生成しないようにします。結果。1つの例は、ObjectCacheを持つdoctrineです。これにより、オブジェクトをデータベースに保存せずに作成でき、オブジェクトは常にキャッシュから取り出されるため、テストはすべて合格します。

この問題はさまざまな方法で解決できます。

TestDoubleであり、実際のAPIから期待される結果をエミュレートする実際のクラスを作成します。これは実際には非常に簡単です。通常のMyApiClientTestDoubleと同じ署名で新しいMyApiClientを作成し、必要に応じてメソッド本体を変更するだけです。

Service.ymlには、次のようなものがあります。

parameters:
  myApiClientClass: Namespace\Of\MyApiClient

service:
  myApiClient:
    class: %myApiClientClass%

この場合、config_test.ymlに以下を追加することで、どのクラスを取得するかを簡単に上書きできます。

parameters:
  myApiClientClass: Namespace\Of\MyApiClientTestDouble

これで、サービスコンテナはテスト時にTestDoubleを使用します。両方のクラスが同じ署名を持っている場合、それ以上何も必要ありません。これがDIExtras Bundleで機能するかどうか、またはどのように機能するかはわかりません。しかし、私は方法があると思います。

または、ApiDoubleを作成して、外部APIと同じように動作するが、テストデータを返す「実際の」APIを実装することもできます。次に、APIのURIをサービスコンテナ(セッターインジェクションなど)で処理し、適切なAPI(devまたはtestの場合はテスト1、本番環境の場合は実際のAPI)を指すパラメーター変数を作成します。 )。

3番目の方法は少しハックですが、テスト内でいつでもプライベートメソッドを作成できますrequest最初に正しい方法でコンテナを設定し、次にクライアントを呼び出してリクエストを行います。

2
Sgoettschkes

あなたがあなたの問題を解決する方法を見つけたかどうかはわかりません。しかし、これが私が使用したソリューションです。これは、これを見つけた他の人にとっても良いことです。

複数のクライアントリクエスト間でサービスをモックする問題を長い間検索した後、私はこのブログ投稿を見つけました:

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

lyrixxは、別のリクエストを行おうとすると、リクエストごとにカーネルがシャットダウンしてサービスのオーバーライドが無効になる方法について説明しています。

これを修正するために、彼は関数テストにのみ使用される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());
    }
}

このガイドに従った後、新しいAppTestKernelでphpunittestを起動する際に問題が発生しました。

Symfonys WebTestCase( https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php )が最初のAppKernelファイルを取得することがわかりました見つけます。したがって、これを回避する1つの方法は、AppTestKernelの名前を変更して、AppKernelの前に配置するか、メソッドをオーバーライドして代わりにTestKernelを取得することです。

ここでは、WebTestCaseのgetKernelClassをオーバーライドして、* 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でロードされ、複数のクライアント要求間でサービスをモックできるようになります。

2
Mibsen

モックを作成します。

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

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

モックオブジェクトのservice_nameを置き換えます。

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

私の問題は使用することでした:

self::$kernel->getContainer();
0
Lebnik