読者です 読者をやめる 読者になる 読者になる

PhalconアプリケーションをCodeceptionでテストしてみた

先日、Codeceptionが1.8にバージョンアップされました。

1.8での変更点の詳細は本家サイトを見ていただくとして、注目すべきは以下の2点。

1つ目は説明不要と思いますが、2つ目は前回のエントリ (Codeception + Selenium2) Acceptance Testに複数種類のブラウザを利用する - think it over の最後に書いた、以下の課題に対する解決策となっています。

複数種類のブラウザを利用してAcceptance Testを実行したい場合、現状では一つのSuitesにつき一種類のブラウザしか指定ができないため、複数Suitesを用意するしかないようです。

前回からの続きで言うと2つ目を取り上げるべきなのでしょうが、Phalconが楽しそうなので先にそちらを。

0. 前提

1. Phalconアプリケーションの準備

1.1. 素材の取得

素材として phalcon/invo · GitHub を利用します。

$ git clone https://github.com/phalcon/invo.git
1.2. DBの初期化
$ echo 'CREATE DATABASE invo' | mysql -u root -p
$ cat schemas/invo.sql | mysql -u root -p invo
1.3. DB接続設定の変更

app/config/config.ini の以下の箇所を適切な接続設定となるよう編集します。

[database]
host     = localhost
username = root
password = secret
name     = invo
1.4. 基底URLの変更

ビルトインサーバーで動作確認をしたいので、
app/config/config.ini の applicationセクションのbaseUrlを以下のように変更します。

[application]
(中略)
baseUri        = /
1.5. ビルトインサーバー用のルーターファイルを作成

Using PHP Built-in webserver — Phalcon 1.2.4 documentation を参考に、invo直下に以下の内容で.htrouter.phpを作成します。

<?php
if (!file_exists(__DIR__ . '/' . $_SERVER['REQUEST_URI'])) {
    $_GET['_url'] = $_SERVER['REQUEST_URI'];
}
return false;
1.6. 動作確認

以下のコマンドでビルトインサーバーを起動します。

$ php -S localhost:8000 -t ./public .htrouter.php

ブラウザから http://localhost:8000にアクセスし、次のような画面が表示されることを確認します。


2. Codeceptionの準備

2.1. インストール・初期化

Composerは時間かかるので、今回はpharを使います。

$ curl -L -O http://codeception.com/codecept.phar

ダウンロードできたら、初期化します。

$ php codecept.phar bootstrap
2.2. DB接続設定

本来であればテスト用DBを別で用意したいところですが、PhalconにはSymfonyで言うところの「env」の概念が備わってなくようなので、今回は上で用意したDBを使います。

modules:
    config:
        Db:
            dsn: 'mysql:host=localhost;dbname=invo'
            user: 'root'
            password: 'your password'
            dump: schemas/invo.sql

dump にはテストデータ投入用のSQLを指定するのですが、invoに同梱されていたものにテストデータが入っているのでそれを使っちゃいましょう。

2.3. Functionalテスト設定の変更

tests/functional.suite.ymlを以下のように編集します。

class_name: TestGuy
modules:
    enabled: [Filesystem, TestHelper, Phalcon1, Db]
    config:
        Phalcon1:
            bootstrap: tests/phalcon_bootstrap.php
            cleanup: true
            savepoints: true

編集後、Guyをビルドします。

$ php codecept.phar build
2.4. CodeceptionがPhalconをテストするためのbootstrapファイルを作成

Phalcon1 Module - Codeception - Documentation にもbootstrapファイルに関する記述がありますが、そんな単純ではなかったです。

ちょっと悩みましたが、以下のようにするのが良いとわかりました。

public/index.phptests/phalcon_bootstrap.phpにコピーし、以下の変更を行います。

82行目付近
voltテンプレートのキャッシュの保存ディレクトリのパスを以下のように変更します。

            "compiledPath" => "../cache/volt/"

            "compiledPath" => "./cache/volt/"


141行目付近
Phalcon1 Module - Codeception - Documentation によると、

The application bootstrap file must return Application object but not call its handle() method.

だそうなので、以下のように変更します。

echo $application->handle()->getContent();

return $application;


これでPhalcon, Codeception共に準備完了です。

3. トップページをテストしてみる

以下のコマンドでテストを作成します。

$ php codecept.phar generate:cept functional IndexController/indexAction

作成された tests/functional/IndexController/indexActionCept.php を以下のように編集します。

<?php
$I = new TestGuy($scenario);
$I->wantTo('test top page');

$I->amOnPage('/');
$I->see('Welcome to INVO', 'h1');

実行します。

$ php codecept.phar generate:cept run functional -d
Codeception PHP Testing Framework v1.8.0.1
Powered by PHPUnit 3.7.28 by Sebastian Bergmann.

Functional Tests (1) -----------------------------------------------
Modules: Filesystem, TestHelper, Phalcon1
--------------------------------------------------------------------
Trying to test top (IndexController\indexActionCept.php)
Scenario:
* I am on page "/"
  [Response] 200
  [Page] http://localhost/
* I see "Welcome to INVO","h1"
 PASSED

--------------------------------------------------------------------


Time: 558 ms, Memory: 7.50Mb

OK (1 test, 1 assertion)

無事成功しました。

4. 登録フォームをテストしてみる

以下のコマンドでテストを作成します。

$ php codecept.phar generate:cept functional SessionController/registerAction

作成された tests/functional/SessionController/registerActionCept.php を以下のように編集します。

<?php
$I = new TestGuy($scenario);
$I->wantTo('test registration form');

$I->amOnPage('/session/register');
$I->fillField('Your Full Name', 'Phalcon Codeception');
$I->fillField('Username', 'phalcon_codeception');
$I->fillField('Email Address', 'demo@example.com');
$I->fillField('Password', 'demo');
$I->fillField('Repeat Password', 'demo');
$I->click("Register");

$I->seeCurrentUrlEquals('/session/register');
$I->seeRecord('Users', ['name' => 'Phalcon Codeception']);

実行します。

$ php codecept.phar run functional SessionController
Codeception PHP Testing Framework v1.8.0.1
Powered by PHPUnit 3.7.28 by Sebastian Bergmann.

Functional Tests (1) -------------------------------------------------
Trying to test registration form (\registerActionCept.php)
Scenario:
* I am on page "/session/register"
* I fill field "Your Full Name","Phalcon Codeception"
* I fill field "Username","phalcon_codeception"
* I fill field "Email Address","demo@example.com"
* I fill field "Password","demo"
* I fill field "Repeat Password","demo"
* I click "Register"
* I see current url equals "/session/register"
* I see record "Users",{"name":"Phalcon Codeception"}
 PASSED

----------------------------------------------------------------------


Time: 1.23 seconds, Memory: 8.25Mb

OK (1 test, 1 assertion)

問題なくテストにパスしました。

5. 最後に

以上、簡単ですが、PhalconアプリケーションもCodeceptionでテストできるようになりました。
フレームワークが変わっても基本的なテストの書き方は一緒。これは本当に素晴らしいです。
もう自分はCodeceptionから離れられません。

(2013/11/22 追記)
 コードを GitHubにアップしました。

(Codeception + Selenium2) Acceptance Testに複数種類のブラウザを利用する

久しぶりの更新です。

CodeceptionのAcceptance TestにSeleniumを利用する場合のTIPSになります。
とある場所にて、Codeceptionのレクチャーをした際に、「IE, ChromeでもAcceptance Testは可能ですか?」という質問をいただきましたので、ここにまとめておきます。

はじめに

このエントリではCodeceptionのバージョン 1.6.10を利用しています。
最新の1.7系ではSelenium2モジュールの改善版であるWebDriverモジュールというものが利用できますので、そちらを利用した方が良いです。

参考

1. Firefox

特殊な設定は必要ないです。
マニュアルどおりにやっていけばできます。

1.1. acceptance.suite.yml
class_name: WebGuy
modules:
    enabled:
        - Selenium2
        - WebHelper
    config:
        Selenium2:
           url: http://localhost:8000
           browser: firefox
           delay: 200
           capabilities:
               unexpectedAlertBehaviour: 'accept'

browserに「firefox」を指定します。

1.2. Seleniumの起動
java -jar selenium-server-standalone-2.37.0.jar

参考:http://codeception.com/docs/modules/Selenium2

2. Internet Explorer

2.1. IEDriverをインストール

SeleniumDownloads ページの

f:id:piccagliani:20131031114958p:plain

からダウンロードして、jarファイルと同じ場所に配置しておきます。

2.2. acceptance.suite.yml

browserの指定を「ie」にします。

modules:
    config:
        Selenium2
           browser: ie

※↑抜粋

2.3. Seleniumの起動
java -jar selenium-server-standalone-2.37.0.jar -Dwebdriver.ie.driver=.\IEDriverServer32.exe

IEDriverを指定して起動します。

3. Chrome

Internet Explorerとほぼ同じです。

3.1. ChromeDriverをインストール

chromedriver - WebDriver for Google Chrome - Google Project Hosting からダウンロードして、jarファイルと同じ場所に配置しておきます。

3.2. acceptance.suite.yml

browserの指定を「chrome」にします。

modules:
    config:
        Selenium2
           browser: chrome

※↑抜粋

3.3. Seleniumの起動
java -jar selenium-server-standalone-2.37.0.jar -Dwebdriver.chrome.driver=.\chromedriver.exe

4. トラブルシューティング・未解決の問題

実際に自分が体験した内容をまとめておきます。
未解決のもの、どなたか知っていたら教えて下さい。

4.1. IE:フォームの文字入力がめっさ遅い!

Selenium の IE Driver で SendKeys したときに1文字ごとの入力が異様に遅い場合の対処 : @jsakamoto と同様、32bit版のドライバを使うことで解決しました。

4.2. (未解決)IE:texteareaにplaceholderが設定されている場合にテストが失敗する

フォームの初期表示で「何も入力されていないよね?」というテストをしたく、

<textarea class="form-control" id="Detail" placeholder="詳細" rows="10" name="task[detail]"></textarea>

というtextareaに対して、

<?php
$I->seeInField('詳細', '');

と記述していましたが、placeholderに設定した値がすでに入力されているとみなされてしまうのか、テストに失敗しました。

4.3. (未解決)Chrome: 日本語のフォーム入力がおかしくなる。
<?php
$I->fillField('題名', '受入試験を行う');
$I->fillField('詳細', 'CodeceptionとSeleniumを上手に使ってね');

f:id:piccagliani:20131031121625p:plain

という悲惨な結果に。。文字化けしているようで、そうでないようにも思える。
Codeception側がおかしいのか、Selenium側がおかしいのか。。。

4.3. (未解決)Chrome:「input type="date"」に対して入力できない
<?php
$I->fillField('開始日', '2013-10-31');

が落ちます。。。

最後に

複数種類のブラウザを利用してAcceptance Testを実行したい場合、現状では一つのSuitesにつき一種類のブラウザしか指定ができないため、複数Suitesを用意するしかないようです。
その際に発生するコードの重複は PageObjects"StepObjects" 等でリファクタリングするのが良いかと。

一つのSuitesで複数種類のブラウザを利用可能とするための提案はすでにされており、
Multiple Selenium2 Sessions for Acceptance tests · Issue #154 · Codeception/Codeception · GitHub
開発者の @davert さん曰く、

f:id:piccagliani:20131031122824p:plain

とのことです。先日1.7がリリースされたばかりなのに、もうバージョン1.8が楽しみです。

(Codeception + Symfony2) セキュアなページに対するFunctional Test

How to simulate Authentication with a Token in a Functional Test (current) - Symfony に書かれている内容をCodeceptionではどのように実現するか。

1. 専用のHelperを書く

<?php
namespace Codeception\Module;

// here you can define custom functions for TestGuy 

use Symfony\Component\BrowserKit\Client;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;

class TestHelper extends \Codeception\Module
{
    public function amLoggedInAs($username)
    {
        $client = $this->getModule('Symfony2')->client;
        $container = $client->getContainer();

        $doctrine = $container->get('doctrine');
        $user = $doctrine
            ->getRepository('VendorSampleBundle:User')
            ->findOneBy(['username' => $username]);

        $firewall = 'secured_area';
        $token = new UsernamePasswordToken($user, null, $firewall, $user->getRoles());

        $session = $container->get('session');
        $session->set('_security_' . $firewall, serialize($token));
        $session->save();

        $cookie = new Cookie($session->getName(), $session->getId());
        $client->getCookieJar()->set($cookie);
    }
}

2. GuyをBuildする

bin/codecept build

3. Helperを使う

<?php
$I = new TestGuy($scenario);
$I->wantToTest('secured area.');

$I->amOnPage('/');
$I->see('Hello Guest!!');

$I->amLoggedInAs('Mike');
$I->amOnPage('/');
$I->see('Hello Mike!!');

備考

上記の解決策は、OAuth認証などアプリ側に認証用のフォームが用意されていない場合や、少しでもテストを早く実行させたい場合に有効になると思います。
テスト対象のアプリがログイン用のフォームを提供している場合は、基本的にはFunctional Testsで説明されている以下のコードのように記述すれば良いです。

<?php
$I = new TestGuy($scenario);
$I->amOnPage('/');
$I->click('Login');
$I->fillField('Username','Miles');
$I->fillField('Password','Davis');
$I->click('Enter');
$I->see('Hello, Miles', 'h1');
?>

ログイン処理がテストの中で頻繁に必要となる場合は、再利用性を高める仕組みである StepObject が1.6.4から導入されています。


以上です

HWIOAuthBundleを使ってSymfony2でOAuth認証を実装する(EntityUserProvider)

HWIOAuthBundleを使ってSymfony2でOAuth認証を実装するの続編です。

今回はアプリ側にユーザー情報を格納するテーブルがあることを前提に、OAuth認証によって得られた情報と連携させてみます。

実現したい内容としては、以下となります。

  1. ログイン用のリンクをクリック
  2. Google側でログイン
  3. アプリ側のユーザー情報を取得
    • ユーザー情報が存在すればそれを利用して認証する
    • もしユーザー情報が存在しなければOAuth認証によって得られた情報をもとにユーザーを作成する

すべてHWIOAuthBundleで実現できれば良いのですが、「ユーザー情報が存在しなければ~」はどうやらできないみたい(参考:Support connect without registration)なのでカスタマイズが必要になります。

1. Entityおよびテーブルを作成

1.1. Entityを作成
php app/console generate:entity --entity=VendorSampleBundle:User

カラムは以下を設定しました。

gid string 255 Google側との連携用カラム
name string 255 ユーザー名
email string 255 メールアドレス
is_active boolean - 有効/無効

作成後、HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUserを継承します。

<?php

namespace Vendor\SampleBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUser;

/**
 * User
 */
class User extends OAuthUser
{
    // ...
}
1.2. テーブルを作成
php app/console doctrine:schema:create

2. OAuthUserProviderを作成し、サービスへ登録

2.1. OAuthUserProviderを作成

前回はHWIOAuthBundleが標準で提供しているOAuthUserProviderを使用しました。
今回は先ほど作ったユーザー情報を格納するテーブルと連携を行うため、EntityUserProviderを継承して新たにsrc\Vendor\SampleBundle\Auth\OAuthUserProvider.phpを作成します。

<?php
namespace Vendor\SampleBundle\Auth;

use HWI\Bundle\OAuthBundle\Security\Core\User\EntityUserProvider;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class OAuthUserProvider extends EntityUserProvider implements UserProviderInterface
{
    public function loadUserByUsername($username)
    {
    }

    public function refreshUser(UserInterface $user)
    {
    }

    public function supportsClass($class)
    {
    }
}

中身は後で実装します。

2.2. OAuthUserProviderをService Containerに登録

src\Vendor\SampleBundle\Resources\config\services.ymlを以下のように書き換えます。

変更前

parameters:
    vendor_sample.oauth_user_provider.class: HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUserProvider

services:
    vendor_sample.oauth_user_provider.service:
        class: %vendor_sample.oauth_user_provider.class%

変更後

parameters:
    vendor_sample.oauth_user_provider.class: Vendor\SampleBundle\Auth\OAuthUserProvider
    vendor_sample.entity.user.class: Vendor\SampleBundle\Entity\User

services:
    vendor_sample.oauth_user_provider.service:
        class: %vendor_sample.oauth_user_provider.class%
        arguments: [@doctrine, %vendor_sample.entity.user.class%, {google: gid}]

OAuthUserProviderインスタンスを生成する際に、親クラスEntityUserProviderコンストラクタ引数に定義されている

  • DBコネクション
  • 利用するUser Entitiyのクラス名
  • OAuth認証との連携用プロパティ

の3つをInjectするように設定します。

3. OAuthUserProviderを実装

を参考にしながら実装していきます。

3.1. loadUserByUsername

本来であれば認証用フォーム等から入力されたユーザー名をもとに該当するEntityを返す処理を書くのだろうが、HWIOAuthBundleによってこいつの代わりにEntityUserProviderloadUserByOAuthUserResponseが利用されるようになるため、実装不要。

たぶん。

念の為確認してみたが、一度も呼ばれなかった。

3.2. refreshUser

認証が必要な箇所へのアクセス毎に呼ばれる。
DBからユーザー情報を取得しなおしたり、内部的な処理を行ったり(最終アクセス時刻の記録など?)する場合は実装するらしい。
今回は特にその必要もないので、そのまま返す。

<?php
    public function refreshUser(UserInterface $user)
    {
        return $user;
    }
3.3. supportsClass

supportsClassによると、引数で与えられたユーザークラスをこのUserProviderがサポートするかどうかを返せば良いらしいので、以下のように実装。
いつ、どのような場面で呼ばれるのか不明。

<?php
    public function supportsClass($class)
    {
        return 'Vendor\\SampleBundle\\Entitiy\\User' === $class;
    }
3.4. コンストラクタ、loadUserByOAuthUserResponseをOverride

loadUserByOAuthUserResponseの標準実装は次のようになっています。

<?php
    public function loadUserByOAuthUserResponse(UserResponseInterface $response)
    {
        $resourceOwnerName = $response->getResourceOwner()->getName();

        if (!isset($this->properties[$resourceOwnerName])) {
            throw new \RuntimeException(sprintf("No property defined for entity for resource owner '%s'.", $resourceOwnerName));
        }

        $username = $response->getUsername();
        $user = $this->repository->findOneBy(array($this->properties[$resourceOwnerName] => $username));

        if (null === $user) {
            throw new UsernameNotFoundException(sprintf("User '%s' not found.", $username));
        }

        return $user;
    }

冒頭に書いた、

  • ユーザー情報が存在すればそれを利用して認証する
  • もしユーザー情報が存在しなければOAuth認証によって得られた情報をもとにユーザーを作成する

のうち、前者しか実現できていないのがわかると思います。

なので、loadUserByOAuthUserResponseをOverrideします。
作成したユーザーを永続化するためにEntityManagerを使いたいので、以下のようにコンストラクタをOverrideします。

<?php
    protected $em;

    public function __construct(ManagerRegistry $registry, $class, array $properties, $managerName = null)
    {
        parent::__construct($registry, $class, $properties, $managerName);
        $this->em = $registry->getManager($managerName);
    }

次にloadUserByOAuthUserResponse

<?php
    public function loadUserByOAuthUserResponse(UserResponseInterface $response)
    {
        try {
            return parent::loadUserByOAuthUserResponse($response);
        } catch (UsernameNotFoundException $e) {
            $rawResponse = $response->getResponse();

            $user = new User($rawResponse['name']);
            $user->setGid($rawResponse['id']);
            $user->setEmail($rawResponse['email']);
            $user->setIsActive(true);
            $this->em->persist($user);
            $this->em->flush();

            return $user;
        }
    }

これで、後者についても実現できます。

最後に

「ユーザー情報が存在しなければ~」を自前で実装しましたが、FOSUserBundleと一緒に使うとひょっとしたらすべて面倒見てくれるかもしれません。

また。今回の私の例では必要なかったので調べていませんが、Reference configurationを見るに、OAuth認証が完了した後にアプリ側に用意した登録フォームでアプリ独自の設定情報をユーザーに登録させるための"connect"という仕組みも提供しているようです。


やっぱり日本語情報が少ないですね。。。

以上です。

HWIOAuthBundleを使ってSymfony2でOAuth認証を実装する

しばらくCodeceptionネタが続く予定だったのですが、Symfony2 + OAuth認証で結構はまったのでシェアしておきます。

FacebookTwitterGoogleなど、ソーシャルアカウントでログインする機能を作成したい場合、各ベンダーが提供しているAPIを利用することもできますが、HWIOAuthBundleは複数のリソースオーナーに対応しているので、こちらを利用することにしました。

基本的には本家のドキュメントを見ながら進めていきましたが、一筋縄ではいかない点などがあり、結構はまりました。
本家のドキュメント以外ではAdding HWIOAuthBundle to your Symfony2 project | /* diegocaprioli.com */が参考になると思います。

このエントリではGoogleを利用した単純なOAuth認証を組み込むまでの手順を追ってきます。

1. Symfonyプロジェクトを作成

1.1. 初期化
php composer.phar create-project symfony/framework-standard-edition Sample 2.3.3

※ AcmeDemoBundleは削除します

1.2. HWIOAuthBundleの依存関係を追加
php composer.phar require hwi/oauth-bundle 0.3.*@dev
1.3. バンドルを作成
php app/console generate:bundle

※ Vendor/SampleBundle を作成したとして以降進めていきます。

2. HWIOAuthBundleを設定

Step 1: Setting up the bundle どおりに進めればOKです

2.1. HWIOAuthBundleを有効化

app/AppKernel.php

<?php

use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
            // ...
            new HWI\Bundle\OAuthBundle\HWIOAuthBundle(),
        );
    }
}
2.2. ルーティングをインポート

app/config/routing.yml

hwi_oauth_redirect:
    resource: "@HWIOAuthBundle/Resources/config/routing/redirect.xml"
    prefix:   /connect

3. OAuthUserProviderをService Containerに登録

  • OAuthUserProvider
  • EntityUserProvider(アプリ側にユーザー管理用のテーブルがある場合に利用)
  • FOSUBUserProvider(アプリ側のユーザー管理をFOSUserBundleで行っている場合に利用)

今回は単純なものが実現できればよいので、OAuthUserProviderを利用します。
src\Vendor\SampleBundle\Resources\config\services.ymlに以下を記述します。

parameters:
    vendor_sample.oauth_user_provider.class: HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUserProvider

services:
    vendor_sample.oauth_user_provider.service:
        class: %vendor_sample.oauth_user_provider.class%

4. リソースオーナーを設定

今回はGoogleアカウントを利用します。

4.1. Google側にアプリケーションを登録

「Redirect URIs」、「JavaScript origins」は以下のように設定しておきます。

Redirect URIs:	http://localhost:8000/login/check-google
JavaScript origins:	http://localhost:8000
4.2. リソースオーナーを設定

どおりに進めればOKです

app/config/config.yml

hwi_oauth:
    firewall_name: secured_area

    http_client:
        timeout: 10
        verify_peer: false
        ignore_errors: false
        max_redirects: 1

    resource_owners:
            google:
                type:          google
                client_id:     %google_api_client_id%
                client_secret: %google_api_client_secret%
                scope:         "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile"

http_clientについて
開発環境など、SSLサーバー証明書を検証できない場合、以下のようなエラーになってしまいます。

SSL certificate problem: unable to get local issuer certificate

そのような環境ではverify_peerfalseに設定しましょう。
その他のパラメータについてはConfiguring the HTTP Clientを参照してください。

Google APIのclient_id, client_secretについて
app/config/parameters.ymlに設定した値を参照するようにしましょう。
app/config/parameters.yml.distへの追加も忘れずに。

5. セキュリティ設定

Step 3: Configuring the security layerに記述されている設定そのままではできませんでした。

5.1. security.ymlを設定

app/config/security.yml

security:
    providers:
        oauth_user_provider:
             id: vendor_sample.oauth_user_provider.service

    firewalls:
        secured_area:
            pattern: ^/
            anonymous: true
            oauth:
                resource_owners:
                    google:   "/login/check-google"
                login_path:   /login
                failure_path: /login
                oauth_user_provider:
                    service: vendor_sample.oauth_user_provider.service

    access_control:
        - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }

ドキュメントと違う点は以下となります。

  • security直下にproviders設定が必要
  • secured_area直下にpattern, anonymous設定が必要
5.2. ルーティングを追加

app/config/routing.yml

google_login:
    pattern: /login/check-google

6. トップページ&ログインページを作成

6.1. ルーティングを追加

src/Vendor/SampleBundle/Resources/config/routing.yml

vendor_sample_homepage:
    pattern:  /
    defaults: { _controller: VendorSampleBundle:Default:index }

vendor_sample_login:
    pattern:  /login
    defaults: { _controller: VendorSampleBundle:Default:login }
6.2. アクションを追加
<?php

namespace Vendor\SampleBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class DefaultController extends Controller
{
    public function indexAction()
    {
        return $this->render('VendorSampleBundle:Default:index.html.twig');
    }

    public function loginAction()
    {
        return $this->render('VendorSampleBundle:Default:login.html.twig');
    }
}
6.3. テンプレートを作成

src\Vendor\SampleBundle\Resources\views\Default\index.html.twig

{% if app.user %}
    <h1>Hello {{ app.user.username }}!!</h1>
{% else %}
    <h1>Hello Guest!!</h1>
{% endif %}

src\Vendor\SampleBundle\Resources\views\Default\login.html.twig

<a href="{{ path('hwi_oauth_service_redirect', { 'service' : 'google' }) }}">
Login with Google
</a>
7. 確認

「Login with Google」のリンクをクリックすると、

f:id:piccagliani:20130820113035p:plain

ログインを行うと、

f:id:piccagliani:20130820113039p:plain

承認すると、トップページが表示され、Googleアカウントに設定されている名前が表示されます。


以上です。

続編として「EntityUserProvider」を使うパターンも機会を見つけて書きたいと思います。

それにしてもSymfony2に関係する日本語情報って少ないですね。。。

Codeceptionを使ってTDD Boot Camp 大阪 2.0/課題に挑戦してみた

しばらくはCodeceptionネタが続きます。

社内でTDDの勉強会をする機会があり、課題としてTDD Boot Campのものを利用させていただきました。
どうせなら、ということでCodeceptionを使って挑戦してみました。

ユニットテストのみで、あまりCodeceptionの旨味は引き出せていないとは思いますが、何かの参考になればと思い、公開してみます。

工夫したポイント

課題のところどころに出てくる、以下のような、「出力する」。
私はこれを標準出力に出力すると理解して進めました。

想定外のもの(硬貨:1円玉、5円玉。お札:千円札以外のお札)が投入された場合は、投入金額に加算せず、それをそのまま釣り銭としてユーザに出力する。

標準出力をテストする場合、PHPUnit出力内容のテストで説明されているexpectOutputString()で実現可能なのですが、

<?php
function testOutput()
{
    $this->expectOutputString("foo");
    $obj->echoFoo();

    $this->expectOutputString("bar");
    $obj->echoBar();
}

のように1メソッド内で連続して使用することができないため、不便です。*1

これをCodeceptionのヘルパーを利用することで解決しました。
ヘルパーについての詳しい説明はModulesAndHelpersにあります。

https://github.com/piccagliani/TddStudy/blob/master/tests/_helpers/CodeHelper.php

<?php
namespace Codeception\Module;

// here you can define custom functions for CodeGuy 

class CodeHelper extends \Codeception\Module
{
    public function seeInStandardOutput($expect, \Closure $func)
    {
        ob_start();
        ob_implicit_flush(false);
        $result = $func();
        $output = ob_get_contents();
        ob_end_clean();
        $this->assertContains($expect, $output);
        return $result;
    }
}

ヘルパーに追加した機能を利用できるようにするためには、vendor\bin\codecept.bat buildする必要があります。

これを、以下のように使います。(VendingMachineTestから抜粋)

<?php
        $insertMoney = function($amount) use ($V) {
            return function() use ($V, $amount) {
                $V->insertMoney(new Money($amount));
            };
        };

        $I->expect("1円玉が投入された場合は、それをそのまま釣り銭としてユーザに出力する。");
        $I->seeInStandardOutput("釣り: 1円", $insertMoney(1));

        $I->expect("5円玉が投入された場合は、それをそのまま釣り銭としてユーザに出力する。");
        $I->seeInStandardOutput("釣り: 5円", $insertMoney(5));

最後に

つい先日、バージョン1.6.4がリリースされました。
テストコードのリファクタリングに役立つPageObjectsStepObjectが追加されたり、Extensionの仕組みが導入されたりしているようです。

また、ちらほらtwitterやブログ等でCodeceptionの日本語情報を取り上げる方が増えて来たように感じます。
日本でも流行の兆しが見えて来たか。今後が楽しみです。

*1:間違っていたら教えてください

Codeceptionを使ってみた(3) Functionalテスト

前回FizzBuzzのUnitテストを書きました。

前回までは単にFizzBuzzを生成できるようになったものの、
アプリケーションとしては成り立っていないので、
今回はコンソール出力するアプリケーションを作成し,そのテストを書いてみます。
また、コードカバレッジを取得してみます。

1. CLIアプリケーションの作成

FizzBuzzを生成して出力するだけで大したことないです。
プロジェクトルートのappディレクトリ配下に作成しました。

<?php
require_once __DIR__ . "/../vendor/autoload.php";

$generator = new DevMStudy\Tdd\FizzBuzz();
$fizzBuzz = $generator->generate(1, 100);
foreach ($fizzBuzz as $line) {
    echo $line . "\n";
}

2. functionalテストの作成


前回はunitテストでしたが、今回はfunctionalテストを生成します。

2.1. テストの生成

C:\workspace\proj>vendor\bin\codecept.bat generate:cept functional fizz_buzz
Test was created in fizz_buzzCept.php

「generate:cept」のほかに、「generate:cest」っていうのもありますが、Codeception Commandsによると、

cept
Generates new empty test file for acceptance and functional tests. Scenario-based test is called Cept, though.
cest
Generates new empty test file for scenario-based unit tests. This file format is called Cest = Cept + Test.

とのこと。むむう。

2.2. CLIモジュールを組み込む

Codeceptionはテストの内容や利用しているフレームワークに応じて適切なモジュールを組み込むことにより、専用のassertを利用できるようになります。
なんだろうなぁ、Guyに「がちょん!」って武器を装着するイメージ?

今回はコンソール出力のテストを行いたいので、Cli Moduleを組み込みます。

  • tests/functional.suite.yml
class_name: TestGuy
modules:
    enabled: [Filesystem, Cli, TestHelper]

「Cli」を追加します。

モジュールを組み込んだ後は、必ず「build」コマンドを用いてGuyに武器を「がちょん」します。

C:\workspace\proj> vendor\bin\codecept.bat build
Building Guy classes for suites: acceptance, functional, unit
WebGuy includes modules: PhpBrowser, WebHelper
WebGuy.php generated successfully. 45 methods added
TestGuy includes modules: Filesystem, Cli, TestHelper
TestGuy.php generated successfully. 14 methods added
CodeGuy includes modules: CodeHelper
CodeGuy.php generated successfully. 0 methods added


2.3. テストの記述

  • tests/functional/fizz_buzzCept.php
<?php
$I = new TestGuy($scenario);
$I->wantTo("test app/fizz_buzz.php");
$I->runShellCommmand("php app/fizz_buzz.php");
$I->seeInShellOutput("1\n2\nFizz\n4\nBuzz");
$I->seeInShellOutput("14\nFizzBuzz\n16");
$I->seeInShellOutput("98\nFizz\nBuzz");
$I->dontSeeInShellOutput("101");

このコードであれば、どのようなテストをしているか、一目瞭然ですね!

ちなみに、「runShellCommmand」は本家がtypoしており、gitの最新版では修正されています。
参考:Fix typo in runShellCommand method by akuzemchak · Pull Request #394 · Codeception/Codeception · GitHub

【追記】バージョン1.6.4で修正がリリースされました。

2.4. テストの実行

C:\workspace\proj>vendor\bin\codecept.bat run functional --steps
Codeception PHP Testing Framework v1.6.3.1
Powered by PHPUnit 3.7.21 by Sebastian Bergmann.

Suite functional started
Trying to test app/fizz_buzz.php (fizz_buzzCept.php)
Scenario:
* I run shell commmand "php app/fizz_buzz.php"
* I see in shell output "1
2
Fizz
4
Buzz"
* I see in shell output "14
FizzBuzz
16"
* I see in shell output "98
Fizz
Buzz"
* I don't see in shell output "101"
  OK



Time: 0 seconds, Memory: 4.75Mb

OK (1 test, 4 assertions)

3. コードカバレッジ

Code Coverageに沿ってやればすぐできます。

  • codeception.yml に以下を追加
coverage:
    enabled: true
    whitelist:
        include:
            - src/*.php
        exclude: ~
    blacklist:
        include: ~
        exclude: ~

あとは、コードカバレッジを出力するオプションを有効にしてrunします。

C:\workspace\proj>vendor\bin\codecept.bat run --coverage --html --xml
Codeception PHP Testing Framework v1.6.3.1
Powered by PHPUnit 3.7.21 by Sebastian Bergmann.

Suite acceptance started

Suite functional started
Trying to test app/fizz_buzz.php (fizz_buzzCept.php) - Ok

Suite unit started
Trying to test generate fizz buzz succeed (DevMStudy\Tdd\FizzBuzzTest::testGenerateFizzBuzzSucceed) - Ok


Time: 7 seconds, Memory: 8.50Mb

OK (2 tests, 14 assertions)


Code Coverage Report
  2013-07-08 11:04:33

 Summary:
  Classes: 100.00% (1/1)
  Methods: 100.00% (1/1)
  Lines:   100.00% (12/12)

\DevMStudy\Tdd::FizzBuzz
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% ( 12/ 12)

XML, HTML形式のレポートは「tests/_log」配下に生成されます。

以上です。
いったん「使ってみた」シリーズはこれでおしまいにします。
CodeceptionはSeleniumと連携してのテストも可能ですので、溜まってきたらまた書きます。