23

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

標準のPHPUnitモックビルダーを使用してクラスのモックバージョンを作成することに問題はありません。私が抱えている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()

4

7 に答える 7

9

を呼び出すと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)));
}

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

于 2013-11-13T10:29:18.403 に答える
8

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

https://github.com/PolishSymfonyCommunity/SymfonyMockerContainer

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

于 2013-07-03T17:47:44.073 に答える
2

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を使用します。両方のクラスが同じ署名を持っている場合、それ以上は何も必要ありません。これがDIExtrasBundleで機能するかどうか、またはどのように機能するかはわかりません。しかし、私は方法があると思います。

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

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

于 2013-03-11T22:05:39.763 に答える
2

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

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

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

于 2013-10-31T11:24:29.430 に答える
2

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

        .....
于 2015-10-01T04:28:01.083 に答える
0

モックを作成します。

$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();
于 2017-05-25T16:24:10.520 に答える
0

私はSymfony4.4でも同じ問題に直面しました。

Symfonyを使用したAPI機能テストでモックを作成するを読んだ後 、解決策を見つけました-self::ensureKernelShutdown()

...    
$client->request('GET', '/any/url/');
$this->assertEquals('stdClass', get_class($client->getContainer()->get($keyName))); // Passes.

self::ensureKernelShutdown()

$client->request('GET', '/any/url/');
$this->assertEquals('stdClass', get_class($client->getContainer()->get($keyName))); // Passes.
...
于 2020-07-21T19:38:26.060 に答える