Home | Symfony2Doc »ガイドブック »テスト

コンテンツ上部に更新日の記載のないページは、翻訳の内容が2.0相当のものになっております。最新の内容は原文のページをご確認ください。

Note

  • 対象バージョン:2.3
  • 翻訳更新日:2013/11/23

テスト

ソースコードに新しい行を1行追加するたびに、潜在的に新しいバグを追加しているかもしれません。 より優れた、信頼性の高いアプリケーションを構築するには、ユニットテストとファンクショナルテストの両方でコードをテストするべきです。

PHPUnit テスティングフレームワーク

Symfony2 ではテスティングフレームワークとして、独立したライブラリである PHPUnit を利用します。 PHPUnit についてここでは詳しく解説しませんが、PHPUnitのドキュメントを読んでおくことをおすすめします。

Note

Symfony2 では PHPUnit 3.5.11 以降が必要です。 Symfony のコアをテストする場合はバージョン 3.6.4 以降を利用して下さい。

ユニットテストかファンクショナルテストかに関わらず、テストは通常の PHP クラスとして実装します。 標準で、テストはバンドルの Tests/ ディレクトリ以下に置きます。 この標準に従う限り、全てのテストを以下のコマンドで実行出来ます。

# コマンドラインオプションで設定ファイルのあるディレクトリを指定
$ phpunit -c app/

-c オプションで PHPUnit の設定ファイルのあるディレクトリを指定します。 PHPUnit のオプションについては app/phpunit.xml.dist ファイルを参照して下さい。

ユニットテスト

通常、ユニットテストは単一の PHP クラスを対象として記述するテストです。 クラスをまたいだアプリケーション全体の動作のテストについては、ファンクショナルテストを参照して下さい。

Symfony2 におけるユニットテストは、標準的な PHPUnit のテストと同様に記述できます。 次のような Calculator クラスがバンドルの Utility/ ディレクトリの中にあるとします。

// src/Acme/DemoBundle/Utility/Calculator.php
namespace Acme\DemoBundle\Utility;

class Calculator
{
    public function add($a, $b)
    {
        return $a + $b;
    }
}

これをテストするには、CalculatorTest クラスをバンドルの Tests/Utility ディレクトリに作ります。

// src/Acme/DemoBundle/Tests/Utility/CalculatorTest.php
namespace Acme\DemoBundle\Tests\Utility;

use Acme\DemoBundle\Utility\Calculator;

class CalculatorTest extends \PHPUnit_Framework_TestCase
{
    public function testAdd()
    {
        $calc = new Calculator();
        $result = $calc->add(30, 12);

        // assert that your calculator added the numbers correctly!
        $this->assertEquals(42, $result);
    }
}

Note

規約として、Tests/ 以下のサブディレクトリはバンドル内の他のディレクトリ構造と一致させます。 したがって、Utility/ ディレクトリ内にあるクラスのテストは Tests/Utility/ ディレクトリ以下に配置します。

通常のアプリケーション同様、オートロードは app/bootstrap.php.cache ファイルによって自動的に有効になります(デフォルトで phpunit.xml.dist ファイルにて設定されています)。

ファイルやディレクトリを指定してテストを実行するには、次のようにします。

# Utility ディレクトリ内の全てのテストを実行
$ phpunit -c app src/Acme/DemoBundle/Tests/Utility/

# Calculator クラスのテストを実行
$ phpunit -c app src/Acme/DemoBundle/Tests/Utility/CalculatorTest.php

# バンドル全体のテストを実行
$ phpunit -c app src/Acme/DemoBundle/

ファンクショナルテスト

ファンクショナルテストでは、ルーティングからビューまでの、アプリケーションのさまざまなレイヤー間の結合テストを行います。 PHPUnit でのテストの記述としては、ファンクショナルテストはユニットテストと違いはありませんが、ファンクショナルテストでは、次のような特殊なワークフローでテストを行います。

  • リクエストの作成
  • レスポンスのテスト
  • リンクのクリック、またはフォームの送信
  • レスポンスのテスト
  • クリーンアップと繰り返し

最初のファンクショナルテスト

ファンクショナルテストは通常の PHP ファイルで、バンドルの Tests/Controller ディレクトリ以下に配置します。 例えば DemoController クラスによって生成されるページのテストを書くには、まず WebTestCase クラスを継承した DemoControllerTest.php クラスを作る所から始めます。

Symfony2 Standard Edition に同梱されている DemoController 用のシンプルなファンクショナルテスト DemoControllerTest を見てみましょう。

// src/Acme/DemoBundle/Tests/Controller/DemoControllerTest.php
namespace Acme\DemoBundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class DemoControllerTest extends WebTestCase
{
    public function testIndex()
    {
        $client = static::createClient();

        $crawler = $client->request('GET', '/demo/hello/Fabien');

        $this->assertGreaterThan(
            0,
            $crawler->filter('html:contains("Hello Fabien")')->count()
        );
    }
}

Tip

ファンクショナルテスト実行時には、アプリケーションのカーネルを WebTestCase クラスが準備します。 これは通常自動で行われますが、カーネルが標準のディレクトリにない場合は、phpunit.xml.dist を修正して KERNEL_DIR 環境変数にカーネルのディレクトリを設定して下さい。

<phpunit>
    <!-- ... -->
    <php>
        <server name="KERNEL_DIR" value="/path/to/your/app/" />
    </php>
    <!-- ... -->
</phpunit>

createClient() 静的メソッドは、Web ブラウザのように動作するクライアントを返します。

$crawler = $client->request('GET', 'hello/Fabien');

request() メソッドは Crawler オブジェクトを返します。 このオブジェクトを使って、レスポンス内の要素を選択したり、リンクをクリックしたり、フォームを送信したりできます。 (request() メソッドについて詳しくはこちらを参照して下さい)

Tip

Crawler オブジェクトは、レスポンスの内容が XML ドキュメント、または HTML ドキュメントの場合にのみ取得出来ます。 そうでない場合は $client->getResponse()->getContent() のようにしてレスポンスの内容を取得します。

リンクをクリックするには、最初にCrawlerオブジェクトで XPath 式や CSS セレクタを使ってリンクを選択し、Client オブジェクトを使ってクリックします。 例えば、以下のコードは Greet という文字列を含む全てのリンクの中から 2 番目のものを選択し、クリックします。

$link = $crawler->filter('a:contains("Greet")')->eq(1)->link();

$crawler = $client->click($link);

フォームの送信の仕方もほとんど同じです。フォームのボタンを選択し、フォームの値を設定して、送信を実行します。

$form = $crawler->selectButton('submit')->form();

// フォームの値を設定
$form['name'] = 'Lucas';
$form['form_name[subject]'] = 'Hey there!';

// フォームを送信
$crawler = $client->submit($form);

Tip

フォームには、ファイルアップロード機能や、様々なフィールドを扱うためのメソッド(select()tick() など)が用意されています。 詳しくはこの下のフォームセクションを参照して下さい。

これでアプリケーションの生成するページを自由に遷移できるようになったので、アサーションを使って意図したとおりに遷移していることを確認しましょう。 Crawler オブジェクトを使って特定の DOM エレメントに対してアサーションを設定するには以下のようにします。

// 指定した CSS セレクタにレスポンスがマッチすることを検証する
$this->assertGreaterThan(0, $crawler->filter('h1')->count());

単にある文字列がレスポンスのテキスト全体に含まれているかどうか検証する場合や、レスポンスの形式が XML や HTML ではないような場合は、 次のようにレスポンスのテキスト全体に対して検証することも出来ます。

$this->assertRegExp(
    '/Hello Fabien/',
    $client->getResponse()->getContent()
);

テストクライアント

テスト用の Client オブジェクトは、Web ブラウザのような HTTP クライアントをシミュレートし、Symfony2 アプリケーションに対してリクエストを送信します。

Note

Client オブジェクトは、BrowserKit コンポーネントと Crawler コンポーネントを利用しています。

リクエストの送信

クライアントから Symfony2 アプリケーションへリクエストを送信するには、次のようにします。

$crawler = $client->request('GET', '/hello/Fabien');

request() メソッドは、引数としてHTTPメソッドとURLをとり、Crawler インスタンスを返します。

レスポンスからDOM要素を探すには Crawler オブジェクトを使います。見つかった要素を使って、リンクのクリックやフォームの送信を行えます。

$link = $crawler->selectLink('Go elsewhere...')->link();
$crawler = $client->click($link);

$form = $crawler->selectButton('validate')->form();
$crawler = $client->submit($form, array('name' => 'Fabien'));

click() メソッドや submit() メソッドは Crawler オブジェクトを返します。 これらのメソッドはアプリケーションをブラウズする最適な方法です。 フォームの HTTP メソッドを調べたり、ファイルアップロードの API を利用できたりと、様々な機能を提供してくれます。

Tip

Link オブジェクトと Form オブジェクトの詳細については、Crawlerオブジェクトの節を参照してください。

response() メソッドで、フォームの送信などのより複雑な操作をすることも出来ます。

// フォームの送信
$client->request('POST', '/submit', array('name' => 'Fabien'));

// ファイルアップロードのあるフォームの送信
$client->request(
    'POST',
    '/submit',
    array(),
    array(),
    array('CONTENT_TYPE' => 'application/json'),
    '{"name":"Fabien"}'
);

// ファイルアップロード
use Symfony\Component\HttpFoundation\File\UploadedFile;

$photo = new UploadedFile(
    '/path/to/photo.jpg',
    'photo.jpg',
    'image/jpeg',
    123
);
$client->request(
    'POST',
    '/submit',
    array('name' => 'Fabien'),
    array('photo' => $photo)
);

// HTTP ヘッダを指定して DELETE リクエストを送信
$client->request(
    'DELETE',
    '/post/12',
    array(),
    array(),
    array('PHP_AUTH_USER' => 'username', 'PHP_AUTH_PW' => 'pa$$word')
);

また、各リクエストを独立した PHP プロセスで実行することで、同一のスクリプト内で複数のクライアントを実行した場合の副作用を回避できます。

$client->insulate();

ブラウジング

Client オブジェクトは、実際の Web ブラウザで実行可能なさまざまな操作をサポートしています。

$client->back();
$client->forward();
$client->reload();

// すべての Cookie と履歴を削除
$client->restart();

内部オブジェクトへのアクセス

New in version 2.3: getInternalRequest(), getInternalResponse() メソッドは Symfony 2.3 で追加されました。

Client オブジェクトを使ってアプリケーションのテストを記述する際に、Client の内部オブジェクトにアクセスしたい場合があるかもしれません。

$history   = $client->getHistory();
$cookieJar = $client->getCookieJar();

直前のリクエストに関する、次のようなオブジェクトも取得できます。

// HttpKernel のリクエストインスタンスを取得
$request  = $client->getRequest();

// BrowserKit のリクエストインスタンスを取得
$request  = $client->getInternalRequest();

// HttpKernel のレスポンスインスタンスを取得
$response = $client->getResponse();

// BrowserKit のレスポンスインスタンスを取得
$response = $client->getInternalResponse();

$crawler  = $client->getCrawler();

リクエストを独立したプロセスで実行していない場合は、Container オブジェクトや Kernel オブジェクトにもアクセスできます。

$container = $client->getContainer();
$kernel    = $client->getKernel();

Container オブジェクトへのアクセス

ファンクショナルテストでは、レスポンスのみをテストすることが推奨されています。しかし、アサーションを記述するために内部オブジェクトにアクセスしたい状況もあるでしょう。このような場合は、次のようにサービスコンテナにアクセスします。

$container = $client->getContainer();

クライアントを独立した PHP プロセスで実行している場合や、HTTP レイヤーを使っている場合は、上のコードでサービスコンテナを取得することはできない点に注意してください。 アプリケーションで利用可能なサービスの一覧は app/console container:debug で参照出来ます。

Tip

チェックしたい情報をプロファイラから取得できる場合は、サービスコンテナの代わりにプロファイラを使ってください。

プロファイリングデータの取得

リクエストのプロファイラを有効にすれば、リクエストの内部処理の情報を取得することが出来ます。 プロファイラを利用すれば、あるページのリクエスト中に実行される DB リクエストが一定回数以下であるかどうかなどを確認出来ます。

プロファイラは以下のようにして取得できます。

// 次に実行するリクエストのプロファイラを有効にする
$client->enableProfiler();

$crawler = $client->request('GET', '/profiler');

// プロファイラを取得
$profile = $client->getProfile();

プロファイラの詳細については、クックブックのファンクショナルテストでプロファイラを使用する方法を参照して下さい。

リダイレクト

リクエストの結果がリダイレクトだった場合でも、クライアントは自動ではリダイレクト先へ遷移しません。 followRedirect() メソッドで明示的に遷移させる必要があります。

$crawler = $client->followRedirect();

全てのリダイレクトに対して自動的に遷移させたい場合は followRedirects() メソッドを使用します。

$client->followRedirects();

Crawlerオブジェクト

Client オブジェクトからリクエストを送信すると、Crawler インスタンスが返されます。 この Crawler を使って、HTML ドキュメントを走査し、ノードを選択し、リンクやフォームを検索します。

DOM の走査

Crawler には、jQuery に似た、HTML/XML ドキュメントの DOM を走査するメソッドがあります。 例えば以下のようにすると、input[type=submit] にマッチするエレメントを検索し、そのうち最後の要素を選択し、さらにその直近の親エレメントを取得します。

$newCrawler = $crawler->filter('input[type=submit]')
    ->last()
    ->parents()
    ->first()
;

他にもたくさんのメソッドがあります。

メソッド 説明
filter('h1.title') CSS セレクタにマッチするノード
filterXpath('h1') XPath 式にマッチするノード
eq(1) 指定したインデックスのノード
first() 最初のノード
last() 最後のノード
siblings() 兄弟のノード
nextAll() 後の兄弟ノード
previousAll() 前の兄弟ノード
parents() 親ノード、先祖ノード
children() 子ノード
reduce($lambda) callable が false を返さないノード

各メソッドは条件にマッチした新しい Crawler オブジェクトを返すので、チェインさせていくことで、インタラクティブにノードを絞り込んでいくことができます。

$crawler
    ->filter('h1')
    ->reduce(function ($node, $i)
    {
        if (!$node->getAttribute('class')) {
            return false;
        }
    })
    ->first();

Tip

count() 関数を使って、現在のCrawlerオブジェクトが保持しているノードの数を取得できます: count($crawler)

情報の抽出

Crawler からノードの情報を抽出できます。

// 最初のノードの、指定した属性の値を返す
$crawler->attr('class');

// 最初のノードの値を返す
$crawler->text();

// すべてのノードから、配列で指定した属性の値を抽出する(_text はノードの値を返す)
$crawler->extract(array('_text', 'href'));

// 各ノードに対してラムダを実行し、結果を配列として返す
$data = $crawler->each(function ($node, $i)
{
    return $node->getAttribute('href');
});

リンク

リンクの選択は上述の走査メソッドでも可能ですが、selectLink() メソッドを使うとより簡単です。

$crawler->selectLink('Click here');

これで、指定された文字列を含むテキストリンク、または alt 属性に指定された文字列を含むリンク付き画像が選択出来ます。 他の走査メソッド同様、これも Crawler オブジェクトを返します。

リンクの Crawler オブジェクトからは Link オブジェクトを取得できます。 Link オブジェクトを使って getMethod()getUri() などの便利なメソッドを利用することが出来ます。 リンクをクリックするには、クライアントの click() メソッドに取得した Link オブジェクトを渡します。

$link = $crawler->selectLink('Click here')->link();

$client->click($link);

Tip

links() メソッドは、すべてのノードの Link オブジェクトの配列を返します。

フォーム

リンクと同様、selectButton() メソッドを使ってフォームを選択できます。

$buttonCrawlerNode = $crawler->selectButton('submit');

Note

この処理では、フォーム自体ではなく、フォームのボタンを選択していることに注意してください。フォームには複数のボタンが存在する可能性があります。走査APIを使う際に、単一のボタンを特定する必要があることを覚えておいてください。

selectButton() メソッドで button タグを選択し、 input タグの内容を送信します。 ボタンを選択するために利用できる値がいくつかあります。

  • value 属性の値
  • 画像の id または alt 属性の値
  • button タグの id または name 属性の値

ボタンに対応するノードが見つかったら、form() メソッドでボタンノードを囲んでいる Form インスタンスを取得できます。

$form = $buttonCrawlerNode->form();

form() メソッドを呼び出す際に、フィールドの値を配列として渡すことで、フォームのデフォルト値を上書きできます。

$form = $buttonCrawlerNode->form(array(
    'name'              => 'Fabien',
    'my_form[subject]'  => 'Symfony rocks!',
));

また、フォームで特定の HTTP メソッドをシミュレートしたい場合は、2 つ目の引数に指定します。

$form = $buttonCrawlerNode->form(array(), 'DELETE');

Client から Form インスタンスを送信します。

$client->submit($form);

フィールドの値は submit() メソッドの2つ目の引数で渡すこともできます。

$client->submit($form, array(
    'name'              => 'Fabien',
    'my_form[subject]'  => 'Symfony rocks!',
));

さらに複雑な状況の場合は、Form インスタンスを配列のようにアクセスして、各フィールドの値を個別に設定できます。

// フィールドの値を変更
$form['name'] = 'Fabien';
$form['my_form[subject]'] = 'Symfony rocks!';

フィールドのタイプごとに、値を操作する便利なAPIが用意されています。

// radioのオプションを選択
$form['country']->select('France');

// checkboxをチェック
$form['like_symfony']->tick();

// ファイルをアプロード
$form['photo']->upload('/path/to/lucas.jpg');

Tip

フォームに送信される値は Form オブジェクトの getValues() メソッドで取得できます。 アップロードされたファイルにアクセスするには、getFiles() メソッドの戻り値の配列を使います。 getPhpValues()getPhpFiles() は、送信された値をPHPフォーマットで返します(角括弧記法のキーをPHPの配列へ変換します)。

テストの設定

ファンクショナルテストで紹介した Client オブジェクトは、テスト用の test 環境でカーネルを生成します。 test 環境では Symfony は app/config/config_test.yml をロードするため、テスト専用の設定に調整することが出来ます。

例えば、Swift Mailer はデフォルトで test 環境では実際にメールを送信しないように設定されています。 これは swiftmailer 設定オプションで確認出来ます。

  • YAML
    # app/config/config_test.yml
    
    # ...
    swiftmailer:
        disable_delivery: true
    
  • XML
    <!-- app/config/config_test.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
                            http://symfony.com/schema/dic/swiftmailer http://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd">
    
        <!-- ... -->
        <swiftmailer:config disable-delivery="true" />
    </container>
    
  • PHP
    // app/config/config_test.php
    
    // ...
    $container->loadFromExtension('swiftmailer', array(
        'disable_delivery' => true,
    ));
    

createClient() メソッドにオプションを渡すことで、デフォルトの環境 (test) やデバッグモードの値 (true) を変更できます。

$client = static::createClient(array(
    'environment' => 'my_test_env',
    'debug'       => false,
));

アプリケーションの動作がHTTPヘッダーに依存している場合、createClient() メソッドの第2引数として渡すことが出来ます。

$client = static::createClient(array(), array(
    'HTTP_HOST'       => 'en.example.com',
    'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
));

リクエストごとにHTTPヘッダーの値を変更することもできます。

$client->request('GET', '/', array(), array(), array(
    'HTTP_HOST'       => 'en.example.com',
    'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
));

PHPUnit の設定

アプリケーションの PHPUnit の設定は phpunit.xml.dist に記述されています。 このファイルを直接編集するか、phpunit.xml ファイルを作ってローカルマシン用に設定をカスタマイズ出来ます。

Tip

バージョン管理システムのリポジトリには phpunit.xml.dist ファイルのみ保存し、phpunit.xml ファイルは無視するよう設定してください。

デフォルトでは phpunit コマンドは、標準的なディレクトリ構成のバンドル内のテスト(src/*/Bundle/Tests/ または src/*/Bundle/*Bundle/Tests/ ディレクトリ以下のテスト)だけ実行します。 テストのディレクトリを追加するのは簡単です。 例えば次のように設定すると、サードパーティのバンドルにあるテストが追加されます。

<!-- hello/phpunit.xml.dist -->
<testsuites>
    <testsuite name="Project Test Suite">
        <directory>../src/*/*Bundle/Tests</directory>
        <directory>../src/Acme/Bundle/*Bundle/Tests</directory>
    </testsuite>
</testsuites>

コードカバレッジに別のディレクトリを追加するには、<filter>セクションも併せて編集してください。

<!-- ... -->
<filter>
    <whitelist>
        <directory>../src</directory>
        <exclude>
            <directory>../src/*/*Bundle/Resources</directory>
            <directory>../src/*/*Bundle/Tests</directory>
            <directory>../src/Acme/Bundle/*Bundle/Resources</directory>
            <directory>../src/Acme/Bundle/*Bundle/Tests</directory>
        </exclude>
    </whitelist>
</filter>
blog comments powered by Disqus