このエントリーは Symfony Advent Calendar 2015 18日目の記事です。
昨日は tarokamikaze さんの 「Symfony初心者がつまづきがちなポイント」 でした。

はじめに

CodeceptionはPHPのテスティング・フレームワークです。今回はこれをSymfonyプロジェクトに適用することを試してみました。
以前からCodeceptionは気になっていたのですが、Using Codeception for Symfony Projects としてSymfony向けの記事が出ていたことから試してみようと思った次第です。
なお、手元で試したSymfonyのデモ・プロジェクトを以下のリポジトリに置いておきましたのでご参考まで。

  • https://github.com/naoyes/2015-dec-quartetcom-tech-blog-codeception-symfony-demo-sample

また、当エントリはsymfonyコマンド (Symfony Installer)および composer の使用が前提になっています。

1. Symfonyプロジェクトを用意

今回はsymfony-demoを利用することにします。

ちなみにこのsymfony-demoはブラウザで実際にアプリケーションを操作しつつ当該ページを生成するのにSymfonyではどのようなソースコードになっているのかを見ることができます。このソースコードはSymfony Best Pracitecesに準拠しているので安心して参考にできますね。

$ symofny demo
$ cd symfony_demo

2. Codeceptionをインストール

まずはCodeceptionをSymfonyプロジェクトにインストールします。

$ composer require --dev "codeception/codeception:~2.1"

コマンド実行時に以下のようにエラーが出ることがあります。

$ composer require --dev "codeception/codeception:~2.1"
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - codeception/codeception 2.1.4 requires php >=5.4.0 -> your PHP version (5.5.20) or "config.platform.php" value does not satisfy that requirement.
    - codeception/codeception 2.1.3 requires php >=5.4.0 -> your PHP version (5.5.20) or "config.platform.php" value does not satisfy that requirement.
    - codeception/codeception 2.1.2 requires php >=5.4.0 -> your PHP version (5.5.20) or "config.platform.php" value does not satisfy that requirement.
    - codeception/codeception 2.1.1 requires php >=5.4.0 -> your PHP version (5.5.20) or "config.platform.php" value does not satisfy that requirement.
    - codeception/codeception 2.1.0 requires php >=5.4.0 -> your PHP version (5.5.20) or "config.platform.php" value does not satisfy that requirement.
    - Installation request for codeception/codeception ~2.1 -> satisfiable by codeception/codeception[2.1.0, 2.1.1, 2.1.2, 2.1.3, 2.1.4].


Installation failed, reverting ./composer.json to its original content.

この際にはcomposer.jsonを以下のように設定することでインストールできるようになりました。

# composer.json

    "config": {
        "bin-dir": "bin",
        "platform": {
-            "php": "5.3.9"
+            "php": "5.5.20"
        }
    },

3. テストの設置

ユニットテスト

環境準備

$ php bin/codecept bootstrap --empty src/AppBundle --namespace AppBundle

src/AppBundle直下に設定ファイルcodeception.ymlが作成され、src/AppBundle/Testsというディレクトリに必要なファイルが作成されました。

$ php bin/codecept generate:suite unit -c src/AppBundle

src/AppBundle配下にユニットテストに必要なテストスイートが登録されました。 ただし、作成されたsrc/AppBundle/Tests/unit.suite.ymlを以下のように変更しないと次のステップには進めませんでした。

# src/AppBundle/Tests/unit.suite.yml

class_name: UnitTester
modules:
    enabled:
-        - \AppBundleHelper\Unit
+        - \AppBundle\Helper\Unit

次にアクタクラスというクラスを生成する必要があるようです。

$ php bin/codecept build -c src/AppBundle
Building Actor classes for suites: unit
 -> UnitTesterActions.php generated successfully. 0 methods added
AppBundle\UnitTester includes modules: \AppBundle\Helper\Unit
UnitTester.php created.

テストの設置

ユニットテストの雛形を設置します。

$ php bin/codecept generate:phpunit unit Utils/SluggerTest -c src/AppBundle

その後テストを src/AppBundle/Tests/unit/Utils/SluggerTest.php のように実装します。

<?php
// src/AppBundle/Tests/unit/Utils/SluggerTest.php

namespace AppBundle\Utils;

class SluggerTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @dataProvider getSlugs
     */
    public function testSlugify($string, $slug)
    {
        $slugger = new Slugger();
        $result = $slugger->slugify($string);

        $this->assertEquals($slug, $result);
    }

    public function getSlugs()
    {
        return array(
            array('Lorem Ipsum'     , 'lorem-ipsum'),
            array('  Lorem Ipsum  ' , 'lorem-ipsum'),
            array(' lOrEm  iPsUm  ' , 'lorem-ipsum'),
            array('!Lorem Ipsum!'   , 'lorem-ipsum'),
            array('lorem-ipsum'     , 'lorem-ipsum'),
        );
    }
}

その実symfony-demoの提供しているテストsrc/AppBundle/Tests/Utils/SluggerTest.php と記述方法としては変わりありません。
ユニットテストの記述方法にはCodeceptionの利用の有無は影響しませんね。

<?php
// src/AppBundle/Tests/Utils/SluggerTest.php

namespace Tests\Utils;

use AppBundle\Utils\Slugger;

class SluggerTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @dataProvider getSlugs
     */
    public function testSlugify($string, $slug)
    {
        $slugger = new Slugger();
        $result = $slugger->slugify($string);

        $this->assertEquals($slug, $result);
    }

    public function getSlugs()
    {
        return array(
            array('Lorem Ipsum'     , 'lorem-ipsum'),
            array('  Lorem Ipsum  ' , 'lorem-ipsum'),
            array(' lOrEm  iPsUm  ' , 'lorem-ipsum'),
            array('!Lorem Ipsum!'   , 'lorem-ipsum'),
            array('lorem-ipsum'     , 'lorem-ipsum'),
        );
    }
}

テストの実行

以下のように無事にテストが実行されました。

$ php bin/codecept run unit -c src/AppBundle
Codeception PHP Testing Framework v2.1.4
Powered by PHPUnit 4.8.21 by Sebastian Bergmann and contributors.

AppBundle.unit Tests (1) -----------------------------------------------------------------------------------
Utils\SluggerTest::testSlugify | #0                                                                   Ok
Utils\SluggerTest::testSlugify | #1                                                                   Ok
Utils\SluggerTest::testSlugify | #2                                                                   Ok
Utils\SluggerTest::testSlugify | #3                                                                   Ok
Utils\SluggerTest::testSlugify | #4                                                                   Ok
------------------------------------------------------------------------------------------------------------


Time: 1.54 seconds, Memory: 10.25Mb

OK (5 tests, 5 assertions)

機能テスト

環境準備

$ php bin/codecept generate:suite functional -c src/AppBundle

src/AppBundle配下に機能テストに必要なテストスイートが登録されました。 ただし、作成されたsrc/AppBundle/Tests/functional.suite.ymlを以下のように変更しないと次のステップには進めませんでした。

# src/AppBundle/Tests/functional.suite.yml

class_name: FunctionalTester
modules:
    enabled:
-        - \AppBundleHelper\Functional
+        - \AppBundle\Helper\Functional

ここでもアクタクラスというクラスを生成します。

$ php bin/codecept build -c src/AppBundle
Building Actor classes for suites: functional, unit
 -> FunctionalTesterActions.php generated successfully. 0 methods added
AppBundle\FunctionalTester includes modules: \AppBundle\Helper\Functional
FunctionalTester.php created.
 -> UnitTesterActions.php generated successfully. 0 methods added
AppBundle\UnitTester includes modules: \AppBundle\Helper\Unit

テストの設置

機能テストの雛形を設置します。

$ php bin/codecept generate:cest functional BlogCest -c src/AppBundle

その後テストを src/AppBundle/Tests/functional/BlogCest.php のように実装します。

<?php
// src/AppBundle/Tests/functional/BlogCest.php

namespace AppBundle;

use AppBundle\FunctionalTester;
use AppBundle\Entity\Post;

class BlogCest
{
    public function postsOnIndexPage(FunctionalTester $I)
    {
        $I->amOnPage('/en/blog/');
        $I->seeNumberOfElements('article.post', Post::NUM_ITEMS);
    }
}

symfony-demoの提供しているテストsrc/AppBundle/Tests/Controller/BlogControllerTest.php に比べて短くて直感的ですね。

<?php
// src/AppBundle/Tests/Controller/BlogControllerTest.php

namespace AppBundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use AppBundle\Entity\Post;

class BlogControllerTest extends WebTestCase
{
    public function testIndex()
    {
        $client = static::createClient();
        $crawler = $client->request('GET', '/en/blog/');
        $this->assertCount(
            Post::NUM_ITEMS,
            $crawler->filter('article.post'),
            'The homepage displays the right number of posts.'
        );
    }
}

テストの実行

ではテストを実行してみます。

$ php bin/codecept run functional -c src/AppBundle
Codeception PHP Testing Framework v2.1.4
Powered by PHPUnit 4.8.21 by Sebastian Bergmann and contributors.

AppBundle.functional Tests (1) ------------------------------------------------------------------------------------------------------------------------------
Posts on index page (BlogCest::postsOnIndexPage)                                                                                                       Error
-------------------------------------------------------------------------------------------------------------------------------------------------------------


Time: 1.41 seconds, Memory: 10.75Mb

There was 1 error:

---------
1) Failed to posts on index page in AppBundle\BlogCest::postsOnIndexPage (tests/functional/BlogCest.php)

                                                                                    
  [RuntimeException] Call to undefined method AppBundle\FunctionalTester::amOnPage  
                                                                                    
#1  /path/to/symfony_demo/src/AppBundle/tests/functional/BlogCest.php:11
#2  /path/to/symfony_demo/src/AppBundle/tests/functional/BlogCest.php:11
#3  AppBundle\BlogCest->postsOnIndexPage

FAILURES!
Tests: 1, Assertions: 0, Errors: 1.

エラーになりました。これはテスト対象の機能が未実装なのではなく設定が足りないためです。
機能テストが正しく実行されるようにsrc/AppBundle/Tests/functional.suite.ymlSymfony2およびDoctrineモジュールが有効になるように設定してやる必要があります。

# src/AppBundle/Tests/functional.suite.yml

class_name: FunctionalTester
modules:
    enabled:
+        - Symfony2:
+            app_path: '../../app'
+            var_path: '../../app'
+        - Doctrine2:
+            depends: Symfony2
        - \AppBundle\Helper\Functional

再度トライしてみます。

$ php bin/codecept run functional -c src/AppBundle
Codeception PHP Testing Framework v2.1.4
Powered by PHPUnit 4.8.21 by Sebastian Bergmann and contributors.

AppBundle.functional Tests (1) ------------------------------------------------------------------------------------------------------------------------------
Posts on index page (BlogCest::postsOnIndexPage)                                                                                                       Ok
-------------------------------------------------------------------------------------------------------------------------------------------------------------


Time: 651 ms, Memory: 40.50Mb

OK (1 test, 1 assertion)

これで無事にテストが実行されました。

受入テスト

環境準備

$ php bin/codecept bootstrap --empty

プロジェクトルート直下にtestsというディレクトリと設定ファイルcodeception.ymlが作成されました。このtestsに受入テストのコードを配置することにします。

$ php bin/codecept generate:suite acceptance -c ./

プロジェクトルート直下に受入テストに必要なテストスイートが登録されました。

次に、ここでもアクタクラスというクラスを生成します。

$ php bin/codecept build -c ./
Building Actor classes for suites: acceptance
 -> AcceptanceTesterActions.php generated successfully. 0 methods added
\AcceptanceTester includes modules: \Helper\Acceptance
AcceptanceTester.php created.

テストの設置

受入テストの雛形を設置します。

$ php bin/codecept generate:cept acceptance BlogCept -c ./

その後テストを tests/acceptance/BlogCept.php のように実装します。これも機能テストと同様の記述方法ですね。

<?php
// tests/acceptance/BlogCept.php

$I = new AcceptanceTester($scenario);
$I->wantTo('open blog page and see article there');
$I->amOnPage('/');
$I->click('Browse application');
$I->seeInCurrentUrl('blog');
$I->seeElement('article.post');

テストの実行

走らせてみます。

$ php bin/codecept run acceptance -c ./
Codeception PHP Testing Framework v2.1.4
Powered by PHPUnit 4.8.21 by Sebastian Bergmann and contributors.

Acceptance Tests (1) -------------------------------------------------------------------------------------------------------------------------------------------
Open blog page and see article there (BlogCept)                                                                                                           Error
----------------------------------------------------------------------------------------------------------------------------------------------------------------


Time: 1.03 seconds, Memory: 10.50Mb

There was 1 error:

---------
1) Failed to open blog page and see article there in BlogCept (tests/acceptance/BlogCept.php)

                                                                          
  [RuntimeException] Call to undefined method AcceptanceTester::amOnPage  
                                                                          
#1  /path/to/symfony_demo/tests/acceptance/BlogCept.php:4
#2  /path/to/symfony_demo/tests/acceptance/BlogCept.php:4

FAILURES!
Tests: 1, Assertions: 0, Errors: 1.

エラーになりました。tests/acceptance.suite.yml を以下のように変更します。

# tests/acceptance.suite.yml

class_name: AcceptanceTester
modules:
    enabled:
+        - PhpBrowser:
+            url: 'http://127.0.0.1:8000'
+            headers:
+                env: test
         - \Helper\Acceptance

これで無事にテストが実行されました。

$ php bin/codecept run acceptance -c ./
Codeception PHP Testing Framework v2.1.4
Powered by PHPUnit 4.8.21 by Sebastian Bergmann and contributors.

Acceptance Tests (1) ----------------------------------------------------------------------------------------------------------------------
Open blog page and see article there (BlogCept)                                                                                     Ok
-------------------------------------------------------------------------------------------------------------------------------------------

Time: 1.01 seconds, Memory: 14.00Mb

OK (1 test, 2 assertions)

すべての種類のテストを一度に実行する

codeception.yml にユニットテストおよび機能テストのソースコードの場所を規定してやります。

# codeception.yml

 actor: Tester
+include:
+    - src/*Bundle
 paths:
     tests: tests
     log: tests/_output

これで無事に設置してあるすべてのテストが実行されました。

$ php bin/codecept run
Codeception PHP Testing Framework v2.1.4
Powered by PHPUnit 4.8.21 by Sebastian Bergmann and contributors.

Acceptance Tests (1) ----------------------------------------------------------------------------------------------------------------------
Open blog page and see article there (BlogCept)                                                                                     Ok
-------------------------------------------------------------------------------------------------------------------------------------------

[AppBundle]: tests from
/path/to/symfony_demo/src/AppBundle

AppBundle.functional Tests (1) ------------------------------------------------------------------------------------------------------------------------------
Posts on index page (BlogCest::postsOnIndexPage)                                                                                                       Ok
-------------------------------------------------------------------------------------------------------------------------------------------------------------

AppBundle.unit Tests (1) -----------------------------------------------------------------------------------
Utils\SluggerTest::testSlugify | #0                                                                   Ok
Utils\SluggerTest::testSlugify | #1                                                                   Ok
Utils\SluggerTest::testSlugify | #2                                                                   Ok
Utils\SluggerTest::testSlugify | #3                                                                   Ok
Utils\SluggerTest::testSlugify | #4                                                                   Ok
------------------------------------------------------------------------------------------------------------

Time: 865 ms, Memory: 42.50Mb

OK (7 tests, 8 assertions)

まとめ

とりあえずは一通りのテストを書いて実行するところまではできました。なお、Symfony3においてはディレクトリ構造が変更になっているため当エントリのままでは動かないと思います。
現時点ではCodeceptionの詳細な動作の把握や、明確なメリットの享受には至っていません。しかし、機能テストおよび受入テストのヒューマンフレンドリな記述方法は魅力に感じました。
みなさんのテスト環境選択の材料のひとつとして当エントリがわずかばかりでもお役に立てれば幸いです。

明日は @brtriver さんです!