PhalconアプリケーションをCodeceptionでテストしてみた
先日、Codeceptionが1.8にバージョンアップされました。
1.8での変更点の詳細は本家サイトを見ていただくとして、注目すべきは以下の2点。
- Phalcon Framework の正式サポート
- Environments という概念の追加
1つ目は説明不要と思いますが、2つ目は前回のエントリ (Codeception + Selenium2) Acceptance Testに複数種類のブラウザを利用する - think it over の最後に書いた、以下の課題に対する解決策となっています。
複数種類のブラウザを利用してAcceptance Testを実行したい場合、現状では一つのSuitesにつき一種類のブラウザしか指定ができないため、複数Suitesを用意するしかないようです。
前回からの続きで言うと2つ目を取り上げるべきなのでしょうが、Phalconが楽しそうなので先にそちらを。
0. 前提
1. Phalconアプリケーションの準備
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.php
を tests/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)
問題なくテストにパスしました。
(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
2. Internet Explorer
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.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に設定した値がすでに入力されているとみなされてしまうのか、テストに失敗しました。
最後に
複数種類のブラウザを利用してAcceptance Testを実行したい場合、現状では一つのSuitesにつき一種類のブラウザしか指定ができないため、複数Suitesを用意するしかないようです。
その際に発生するコードの重複は PageObjects や "StepObjects" 等でリファクタリングするのが良いかと。
一つのSuitesで複数種類のブラウザを利用可能とするための提案はすでにされており、
Multiple Selenium2 Sessions for Acceptance tests · Issue #154 · Codeception/Codeception · GitHub
開発者の @davert さん曰く、
とのことです。先日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認証によって得られた情報と連携させてみます。
実現したい内容としては、以下となります。
- ログイン用のリンクをクリック
- Google側でログイン
- アプリ側のユーザー情報を取得
- ユーザー情報が存在すればそれを利用して認証する
- もしユーザー情報が存在しなければ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 | ユーザー名 |
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を実装
- How to create a custom User Provider (current) - Symfony
- Symfony\Component\Security\Core\User\UserProviderInterface | Symfony2 API
を参考にしながら実装していきます。
3.1. loadUserByUsername
本来であれば認証用フォーム等から入力されたユーザー名をもとに該当するEntityを返す処理を書くのだろうが、HWIOAuthBundleによってこいつの代わりにEntityUserProvider
のloadUserByOAuthUserResponse
が利用されるようになるため、実装不要。
たぶん。
念の為確認してみたが、一度も呼ばれなかった。
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認証で結構はまったのでシェアしておきます。
FacebookやTwitterやGoogleなど、ソーシャルアカウントでログインする機能を作成したい場合、各ベンダーが提供している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
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_peer
をfalse
に設定しましょう。
その他のパラメータについては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>
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がリリースされました。
テストコードのリファクタリングに役立つPageObjects
やStepObject
が追加されたり、Extensionの仕組みが導入されたりしているようです。
また、ちらほらtwitterやブログ等でCodeceptionの日本語情報を取り上げる方が増えて来たように感じます。
日本でも流行の兆しが見えて来たか。今後が楽しみです。
*1:間違っていたら教えてください
Codeceptionを使ってみた(3) Functionalテスト
前回までは単にFizzBuzzを生成できるようになったものの、
アプリケーションとしては成り立っていないので、
今回はコンソール出力するアプリケーションを作成し,そのテストを書いてみます。
また、コードカバレッジを取得してみます。
1. CLIアプリケーションの作成
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モジュールを組み込む
なんだろうなぁ、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. テストの記述
<?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. コードカバレッジ
- 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と連携してのテストも可能ですので、溜まってきたらまた書きます。