Codeceptionのドキュメントの翻訳をはじめました。
日本のPHPerにもっとCodeceptionを知ってもらうため、ついに?立ち上がりました。
タイトルのとおり、Codeceptionのドキュメントの翻訳をはじめました。
piccagliani/Codeception.docs.ja_JP · GitHub
- 日本語ドキュメント置き場
以前、ポルトガル語に翻訳された方がいて、その方とメイン開発者の@davertさんとのやり取りを見ると、
We won't get modules translated (too much of work, and modules docs are written inside the comments in PHP cide). But guides should be translated.
https://github.com/Codeception/docs.pt_BR/issues/1#issue-19933969
ということなので、guidesのみを対象としています。
現状、ルール等何も用意出来ていない状態で見切り発車している状態ではありますが、
プルリクエストなどお気軽にしていただければと思います。
JenkinsとDockerを使い、WebアプリのE2Eテストをする
前回のエントリの続きです。
今回は単純なアプリのテストなので、Dockerfileもテスト手順もシンプルでした。
より複雑な手順が必要となる、Seleniumを利用したWEBアプリのE2Eテストなどもこの方式でできるのかどうか、引き続き実験してみます。
ということで実験しました。
目次
実際の手順
1. WEBアプリを準備する
Laravel4で、GET /users
するとusersテーブルからユーザー一覧を取得して表示する、という簡単なものを作成しました。
piccagliani/docker-l4-sample · GitHub
テストはCodeceptionのWebDriverモジュールを利用して記述しました。
<?php $I = new AcceptanceGirl($scenario); $I->wantToTest('list users'); $I->amOnPage('/users'); $I->see('Bill Evans', '#users tbody tr:nth-child(1)'); $I->see('Thelonius Monk', '#users tbody tr:nth-child(2)'); $I->see('Bud Powell', '#users tbody tr:nth-child(3)');
2. Dockerコンテナ上でWEBアプリケーションを公開する
Dockerファイルをアプリケーションリポジトリの.docker/Dockerfile
に記述します。
少し悩んだ点は、ApacheやMySQLなどの設定をどのように行えばよいか、ということ。
選択肢として、以下を考えました。
- RUN命令でsedを使い、必要な箇所の置換を行う
- ADD命令でファイルを置き換える
前者は、単純な置換であればいいですが、あっという間に未来のないDockerfileになりそうです。
なので後者でいくことにしました。
Dockerfileと並列に、
を用意し、ADDします。
結果、Dockerfileは以下のようになりました。
FROM centos:6 MAINTAINER @piccagliani RUN cp -p /usr/share/zoneinfo/Asia/Tokyo /etc/localtime RUN echo ZONE="Asia/Tokyo" > /etc/sysconfig/clock RUN echo UTC="false" >> /etc/sysconfig/clock RUN source /etc/sysconfig/clock RUN yum update -y RUN yum install -y git RUN rpm -Uvh http://ftp.jaist.ac.jp/pub/Linux/Fedora/epel/6/i386/epel-release-6-8.noarch.rpm RUN rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm # PHP 5.6 RUN yum install -y --enablerepo=remi --enablerepo=remi-php56 php php-pdo php-mysql php-mbstring php-mcrypt # Composer RUN curl -sS https://getcomposer.org/installer | php RUN mv composer.phar /usr/local/bin/composer # MySQL RUN rpm -Uvh http://dev.mysql.com/get/mysql-community-release-el6-5.noarch.rpm RUN yum install -y mysql-community-server # Add config files ADD httpd.conf /etc/httpd/conf/httpd.conf ADD my.cnf /etc/my.cnf ADD php.ini /etc/php.ini # Apache EXPOSE 80 CMD ["/bin/bash"]
docker run
する際に、 -p 8000:80
オプションをつけてあげれば、コンテナの外部からホストのIPを指定してWEBアプリケーションにアクセスできるようになります。
3. Seleniumコンテナを導入、起動する
Codeceptionの開発者である@davertさんが、Selenium Server, Xvfb, Firefox, Chromiumを導入済みのコンテナイメージを共有してくれています。
詳細は Acceptance Testing With No Selenium or PhantomJS Installed を確認してください。
$ docker pull davert/selenium-env $ docker run -d -p 4444:4444 davert/selenium-env
これでバックグラウンドでコンテナが起動している状態となります。
4. CodeceptionのWebDriverモジュールの設定をする
結果から書くと、以下のような設定を行いました。
class_name: AcceptanceGirl modules: enabled: - AcceptanceHelper - Db - WebDriver config: WebDriver: host: 172.17.42.1 # SeleniumサーバーのIP = DockerホストのIP port: 4444 url: 'http://172.17.42.1:8000/' # テスト対象のIP = これまたDockerホストのIP browser: firefox window_size: 1680x1050 wait: 10 capabilities: unexpectedAlertBehaviour: 'accept'
ちょっと悩んだのは、以下の設定項目。
- url
- Starting URL for your app.
- host
- Selenium server host (127.0.0.1 by default).
- port
- Selenium server port (4444 by default).
コメントとして書いていますが、WEBアプリのコンテナも、SeleniumEnvコンテナも、-p
オプションでポートのマッピングを行っているので、どちらもDockerホストのIPにすればとりあえず動くんじゃね?と。。。
5. テスト実行用のスクリプトを作成する
$APPROOT/.docker/run-tests.sh
を以下のように作成しました。
#!/bin/bash service httpd start service mysqld start cd /opt/docker-l4-sample chown -R 777 app/storage mysql -u root < ./.docker/setup.sql composer install vendor/bin/codecept run --debug
前回と同様、/opt/docker-l4-sample
にはJenkinsのジョブのワークスペースがマウントされます。
また、./.docker/setup.sql
はアプリ用のDB・ユーザーの作成を行います。
6. Jenkinsにジョブを作成し、ビルドする
前回と基本的には同じです。
ジョブ名
リポジトリの名前に合わせました。
ジョブの形式
「フリースタイル・プロジェクトのビルド」を選択。
ビルド手順
「シェルの実行」を選択し、以下を記述。ポートのマッピングオプションを追加しています。
docker build -t $JOB_NAME $WORKSPACE/.docker/ docker run -v $WORKSPACE:/opt/$JOB_NAME -p 8000:80 $JOB_NAME sh /opt/$JOB_NAME/.docker/run-tests.sh
ビルドを実行すると、
コンテナがビルドされ、
run-tests.sh
が実行され
最終的に、こんな出力となり、
Codeception PHP Testing Framework v2.0.9 Powered by PHPUnit 4.4.2 by Sebastian Bergmann. Rebuilding AcceptanceGirl... Acceptance Tests (1) ----------------------------------------------------------------------- Modules: AcceptanceHelper, Db, WebDriver -------------------------------------------------------------------------------------------- Trying to test list users (usersCept) Scenario: * I am on page "/users" * I see "Bill Evans","#users tbody tr:nth-child(1)" * I see "Thelonius Monk","#users tbody tr:nth-child(2)" * I see "Bud Powell","#users tbody tr:nth-child(3)" PASSED -------------------------------------------------------------------------------------------- Functional Tests (1) ----------------------------------------------------------------------- Modules: Filesystem, FunctionalHelper, Db, Laravel4 -------------------------------------------------------------------------------------------- Trying to test list users (usersCept) Scenario: * I am on page "/users" [Response] 200 [Page] http://localhost/users [Cookies] [] [Headers] {"cache-control":["no-cache"],"date":["Fri, 23 Jan 2015 09:49:20 GMT"],"content-type":["text/html; charset=UTF-8"]} * I see "Bill Evans","#users tbody tr:nth-child(1)" * I see "Thelonius Monk","#users tbody tr:nth-child(2)" * I see "Bud Powell","#users tbody tr:nth-child(3)" PASSED -------------------------------------------------------------------------------------------- Rebuilding UnitGirl... Unit Tests (0) ------------------------------ Modules: Asserts, UnitHelper --------------------------------------------- --------------------------------------------- Time: 1.39 minutes, Memory: 18.00Mb OK (2 tests, 6 assertions) Finished: SUCCESS
ビルドが成功しました!
今後の課題など
Jenkinsのジョブが同時に走った場合
今回はSeleniumとWEBアプリ間の通信にホストIPを利用して行いましたが、異なるアプリのE2Eテストが同時に実行された場合を考えると、アプリ毎にポートのマッピングを変えないといけないのではないか?と思います。
Jenkinsにジョブを設定する際に「どのポート空いてるんだっけ?」と考えるのはちょっとイヤですね。
他のホストのコンテナに接続するパターン - ✘╹◡╹✘
などを参考に、コンテナ間の通信はどうあるべきか、について理解する必要がありそうです。
以上となります。
課題はありますが、理想のCIサーバを目指し、また一歩進みました。
JenkinsとDockerを使い、動作環境の異なるアプリをCIする
私が属する組織では受託開発がメインでして、サーバーやミドルウェアなどの要件を我々が自由に決定できないケースもしばしば。
基本はPHPなのですが、案件によってバージョンが異なったり、近頃ではNodeの案件なども出てきました。
長いこと「CIサーバを導入したい」と思いつつもアプリ毎にCIサーバを立ち上げるのもメンドイコスト等の関係で現実的ではないので、頭を悩ませていました。
そんな中、Dockerを使って解決している先人がいましたので、早速、試してみました。
参考サイト
目次
- 前提環境
- 先人の知恵まとめ
- 実際の手順
- 今後の課題など
長いです。。。
先人の知恵まとめ
いろいろと調べていたところ、以下のようなポイントがあることがわかりました。
JenkinsユーザーをDockerグループに所属させる
→JenkinsのジョブでDockerコンテナを操作できるようにするため
JenkinsのworkspaceをDockerにマウントする
→JenkinsとDockerとでシームレスにファイルを利用できるようにするため
Dockerfileはアプリに同梱しておく
→だって開発中にインフラ要件変わるかもでしょ?
アプリ毎に異なる初期設定やテストの手順はスクリプトにまとめてアプリに同梱しておく
→Jenkinsのビルド手順を共通化するため
実際の手順
1. VagrantでJenkins + Dockerのサーバーを用意する
JenkinsとDockerを導入したCentOS6.5をVagrantで構築します。
$ vagrant box add centos65-x86_64-20140116 https://github.com/2creatives/vagrant-centos/releases/download/v6.5.3/centos65-x86_64-20140116.box $ vagrant init centos65-x86_64-20140116
作成されたVagrantfileを編集します。
- ホストオンリーネットワークを有効にする
config.vm.network "private_network", ip: "192.168.33.10"
- 初期設定およびJenkinsとDockerを導入するプロビジョニングコードを記述する
config.vm.provision "shell", inline: <<-SHELL sudo cp -p /usr/share/zoneinfo/Asia/Tokyo /etc/localtime sudo sh -c 'echo ZONE=\"Asia/Tokyo\" > /etc/sysconfig/clock' sudo sh -c 'echo UTC=\"false\" >> /etc/sysconfig/clock' sudo sh -c "source /etc/sysconfig/clock" sudo yum update -y sudo yum install -y wget sudo rpm -ivh http://ftp.jaist.ac.jp/pub/Linux/Fedora/epel/6/i386/epel-release-6-8.noarch.rpm sudo yum install -y docker-io sudo chkconfig docker on sudo yum install -y java-1.7.0-openjdk sudo wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo sudo rpm --import http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key sudo yum install -y jenkins sudo chkconfig jenkins on sudo sed -i -e 's@JENKINS_ARGS=""@JENKINS_ARGS="--prefix=/jenkins"@' /etc/sysconfig/jenkins sudo sed -i -e 's@JENKINS_JAVA_OPTIONS="-Djava.awt.headless=true"@JENKINS_JAVA_OPTIONS="-Djava.awt.headless=true -Dhudson.util.ProcessTree.disable=true"@' /etc/sysconfig/jenkins sudo usermod -G docker jenkins sudo yum clean all SHELL
VMを作成します。
$ vagrant up
成功したらVMを再起動し、ブラウザで、http://192.168.33.10:8080/jenkins
にアクセス、jenkinsが正常に動作していることを確認します。
jenkinsが正常に動作していたら、以下を行っておきます。
2. デモアプリ(PHP, Node)を用意する
3. Dockerファイルを作成する
$APPROOT/.docker/Dockerfile
をそれぞれに作成しました。
PHP
FROM centos:6 MAINTAINER @piccagliani RUN cp -p /usr/share/zoneinfo/Asia/Tokyo /etc/localtime RUN echo ZONE="Asia/Tokyo" > /etc/sysconfig/clock RUN echo UTC="false" >> /etc/sysconfig/clock RUN source /etc/sysconfig/clock RUN yum update -y RUN rpm -Uvh http://ftp.jaist.ac.jp/pub/Linux/Fedora/epel/6/i386/epel-release-6-8.noarch.rpm RUN rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm RUN yum install -y --enablerepo=remi --enablerepo=remi-php56 php php-mbstring php-mcrypt RUN curl -sS https://getcomposer.org/installer | php RUN mv composer.phar /usr/local/bin/composer CMD ["/bin/bash"]
Node
Ubuntu + NodeJS + npmとします。
FROM ubuntu:14.04 MAINTAINER @piccagliani RUN cp -p /usr/share/zoneinfo/Asia/Tokyo /etc/localtime RUN apt-get update RUN apt-get -y upgrade RUN apt-get install -y nodejs npm RUN update-alternatives --install /usr/bin/node node /usr/bin/nodejs 10 CMD ["/bin/bash"]
4. テスト実行用のスクリプトを作成する
それぞれのアプリに$APPROOT/.docker/run-tests.sh
を作成し、テスト実行用のスクリプトを記述しました。
PHP
#!/bin/bash cd /opt/php-fizzbuzz composer install vendor/bin/codecept run
Node
#!/bin/bash cd /opt/node-fizzbuzz npm install npm test
/opt/php-fizzbuzz
、/opt/node-fizzbuzz
には、Jenkinsのワークスペースがマウントされます。
5. Jenkinsにジョブを作成し、ビルドする
それぞれのアプリ用のジョブを以下のように作成しました。
ジョブ名
リポジトリの名前に合わせました。
ジョブの形式
「フリースタイル・プロジェクトのビルド」を選択。
ビルド手順
「シェルの実行」を選択し、以下を記述。
docker build -t $JOB_NAME $WORKSPACE/.docker/ docker run -v $WORKSPACE:/opt/$JOB_NAME $JOB_NAME sh /opt/$JOB_NAME/.docker/run-tests.sh
ジョブが作成できたら、それぞれビルドを実行してみます。
初回はコンテナの作成に時間がかかると思いますが、2度目からはキャッシュが使われるようになるため、ササッといくはずです。
あとは、GitのHookなり、ポーリングなりを利用すればCIできますね!
最後に. 今後の課題など
より複雑なテストはどうなる?
今回は単純なアプリのテストなので、Dockerfileもテスト手順もシンプルでした。
より複雑な手順が必要となる、Seleniumを利用したWEBアプリのE2Eテストなどもこの方式でできるのかどうか、引き続き実験してみます。
パーミッションまわり
今回はユーザー権限まわりを気にしていないので、すべてrootで実行されます。
このあたりも問題になる可能性がありますね。どうするのがBest Practiceなのでしょう?
テスト実行後にコンテナを破棄したい
何度もビルドしていると、以下のように古いコンテナが残ってしまいます。
$ sudo docker ps -a CONTAINER ID IMAGE a858a6348358 php-fizzbuzz:latest 876216e216ac node-fizzbuzz:latest 4f93cfe9961a node-fizzbuzz:latest 582b56657583 node-fizzbuzz:latest a1ef176f0497 php-fizzbuzz:latest
これら、破棄した方がいいと思いますが、jenkinsでどうやるんでしょう?
以上となります。
Docker、「foregroundプロセスがないとダメ」とかいろいろ躓いたところはありましたが、使いこなせるようになると強力ですね。
これでようやくCIサーバーを導入できそうです。
【メモ】 CentOS6.5 + Docker + Jenkins用Vagrant シェルプロビジョニング
マニュアルには載っていないCodeceptionのコツ/TIPS
バージョン2で入った新機能などを紹介するエントリなど書く書く詐欺をしていたら年末になってしまいました。
というか2014年、2つめのエントリ。。。。
そんな今年は
- ZF2 x Codeception
- Laravel4 x Codeception
を中心にお仕事をしておりました。
社内におけるCodeceptionの実績も溜まってきましたので、その経験をもとにした、マニュアルには載っていないちょっとしたコツやTIPSを紹介したい思います。
Actorにはテンションの上がる名前をつけよう
「マニュアルには載っていない」と前置きしながらいきなりマニュアルに載っていることを。。。
バージョン1では「Guy」固定だったActor名、バージョン2からは自由に名前を決めることができます!
(「FunctionalGuy」、「AcceptanceGuy」の「Guy」部分です。)
正直バージョン2の新機能で最も素晴らしいのはココじゃないか!?
実際のActor名の指定はbootstrapのオプションとして指定します。
php codecept.phar bootstrap -a Actor名
好きなアノ子の名前でも、ペットの名前でも、親しみの湧く名前を付けましょう。
「ああ、いま君はこのフィールドに値を入力してくれているんだね!」
「さぁ、クリックしておくれ、submitボタンを!」
「ああ、君には見えたかい?h1タグに刻まれた僕の想いを!」
胸が熱くなること間違いなしです。
楽しくテストしましょう。
CSSのセレクタをシンプルに記述できるようにしよう
$I->see(...)
, $I->seeElement(...)
など、様々な箇所に登場してくるCSSセレクタ。
あれ、テストコードの可読性を下げるやっかいなヤツです。
性格にも依るんじゃないかと思いますが、時にはこんなことに。
<?php $I->see('山田太郎', '#users table tbody tr:nth-child(5) td:nth-child(2)');
まぁ、PageObject使えよ!ということかもしれませんが、
その前にテンプレートのid属性やclass属性をテストを意識したものに整理しておくことをおすすめします。
ただ、テンプレート外注した場合など、idを下手にいじるとデザインに影響が及んだり、もあるわけで。
そんなときはテスト専用のクラスをつけてしまえば良いと開発者の@davertさんはアドバイスしています。
In order to simplify locators used in functional or acceptance tests we add special locator classes into HTML of web pages. So not to confuse them with classes needed by CSS or JavaScript we use lc- prefix. Thus, if you find locator to be to complex, it would be much easier to edit HTML and add a new class into it.
http://phptest.club/t/codeception-tips-tricks/14/8
- フォームであればラベルで
fillField
,seeInField
利用できるように、for属性、id属性の対応を適切にしておく - 一覧系であれば主キーにもとづいたid属性やクラスをtrに付与しておく
などを実践するようにしています。
テストコードの統一性を保つコツ
Functional Test, Acceptance Testは基本的にCeptフォーマットで記述しています。
開発者によって、
- wantTo
- wantToTest
- expect
- expectTo
- comment
に対する理解の違い、使いどころの違いのため、テストコードの統一性が損なわれるという経験をしました。
そこで、現在では以下のようなテンプレートを導入しています。
ぼくのかんがえるさいきょうのCeptてんぷれーと
<?php $I = new FunctionalGirl($scenario); $I->wantToTest('テストしたい機能名'); $I->comment('どのようなテストを行うのかの説明、前提事項など'); $I->amGoingTo('操作内容'); $I->comment('特記事項'); $I->amOnPage(...); $I->fillField(...); $I->click(...); $I->expect('操作の結果、期待すること1'); $I->comment('特記事項'); $I->see(...); $I->seeInField(...) $I->expect('操作の結果、期待すること1'); $I->comment('特記事項'); $I->see(...); $I->seeInField(...)
このように、1つのamGoingToに対して複数のexpectを記述する形式にすると頭の中身をそのまま記述していけると考えています。
いまのところ、これが「さいきょう」だと思ってますが、、、、異論は認める。
例
<?php $I = new FunctionalGirl($scenario); $I->wantToTest('[管理画面] ユーザー登録 入力画面'); $I->comment('このテストではユーザー登録用のフォームが正常に動作することを確認します。'); $I->amGoingTo('ログインしてユーザ登録画面へアクセスする'); $I->amLoggedAs(['email' => 'admin@example.com', 'password' => 'password']); $I->amOnPage("admin/users/create"); $I->expect('フォームの初期値に何も設定されていないこと'); $I->seeInField('ユーザー名', ''); $I->seeInField('メールアドレス', ''); $I->seeInField('パスワード', ''); $I->amGoingTo('何も入力せずに送信ボタンをクリックする'); $I->click("送信"); $I->expect('登録画面が再表示されること'); $I->seeCurrentUrlEquals('admin/users/create'); $I->expect('エラーメッセージが表示されること'); $I->see('入力してください', '#username'); $I->see('入力してください', '#email'); $I->see('入力してください', '#password');
最終手段!モジュールを拡張する方法
CodeceptionはActorへの機能追加を実現するためのHelperという仕組みを提供しています。
Modules and Helpers - Codeception - Documentation
しかしながら、要件によってはモジュール+ヘルパーではテストができないケースがあるかもしれません。
そんなときには最終手段としてモジュール自体を拡張するという方法が残っています。
Laravel4モジュールを例に、拡張の手順を説明します。
(パス等はプロジェクトに合わせて読み替えてください。)
モジュールの拡張手順
1. app/lib/ext/Codeception/ModuleにLaravel4Extクラスを作成
<?php namespace Codeception\Module; class Laravel4Ext extends Laravel4 { }
2. Composerのautoloadに登録
"psr-0": { "Codeception": "app/lib/ext" }
3. テスト設定ファイルを書き換える
たとえば、tests/functional.suite.yml
- 変更前
class_name: FunctionalGirl modules: enabled: [Filesystem, FunctionalHelper, Laravel4] config: Laravel4: cleanup: false unit: true environment: 'testing' filters: true
この、Laravel4をLaravel4Extに変更します。
- 変更後
class_name: FunctionalGirl modules: enabled: [Filesystem, FunctionalHelper, Laravel4Ext] config: Laravel4Ext: cleanup: false unit: true environment: 'testing' filters: true
これで準備は完了です。
あとはLaravel4Extクラスで既存のメソッドをオーバーライドして好きなようにできます。
注意
Codeceptionに限らず「フレームワークの処理をオーバーライドする」というのは、
他をすべて考え尽くしたあとの最終手段として採用すべき、だと個人的には考えています。
将来的にフレームワーク側のバージョンアップに追随できなくなる可能性がある、というのが一番の理由です。
実際に拡張をした例
Laravel4モジュールの場合、DBに対する変更を確認するためのAPIとして、
- seeRecord
- dontSeeRecord
があります。
とあるアプリケーションにて、アプリケーションが使用するユーザーとは別のユーザーが所有するテーブルにデータを永続化する、という機能がありました。
さらに、セキュリティポリシーによりアプリケーションデフォルトのユーザーは永続先テーブルに対する権限は何もありません。
そんな機能をテストするためには、テスト中にデータベースのコネクションを切り替えられるようにしなければなりません。
- Helperを使い
switchDatabaseConnection
を追加する - seeRecord, dontSeeRecordにコネクション名を指定できるようにする
という2つの選択肢を考えました。
前者ではコネクションを戻すのを忘れた場合に後続のテストに影響がある(=うっかりミスで開発効率を下げる可能性がある)ため、後者を採用し、以下のようにしました。
(「コレをしたらアレを必ず」ルールが増えるのは大変です)
<?php public function seeRecord($model, $attributes = array(), $connection = self::DEFAULT_CONNECTION) { \DB::setDefaultConnection($connection); parent::seeRecord($model, $attributes); \DB::setDefaultConnection(self::DEFAULT_CONNECTION); } ?>
1年半前から使いはじめたCodeception、当時のバージョンは1.6.3.1。現在のバージョンは2.0.9。
もっと日本のPHPerに広めたいですね。来年こそはphpconに。。。
以上となります。
Codeceptionにバグ修正をプルリました。
久しぶりの更新です。
Codeception、メジャーバージョンアップが近いです。楽しみです。
ここのところ、Zend Framework2 + Codeception で開発をしていました。
その中でバグを2件見つけてPRしたので、参考までに。
1. input[type=image]をクリックした場合にSubmitされない
<form method="POST" action="/input"> <input type="image" src="button.gif" alt="Submit" /> </form>
というフォームがあった場合に、$I->click(...)
の記述によってSubmitされないケースがありました。
Submitされる
<?php $I->click('Submit');
2. 機能テストでGETパラメータがコントローラに渡らない
ZF2モジュールにバグがありました。
ケース1
<?php $I->amOnPage('/?foo=bar');
GETパラメータfooがコントローラに渡されませんでした。
ケース2
<form method="POST" action="/?foo=bar"> <input type="submit" value="Submit" /> </form>
というフォームに対して、
<?php $I->click('Submit');
とした場合にやはりGETパラメータfooがコントローラに渡されませんでした。
2つのPRは無事にマージされたので、おそらく1.8.5にてリリースされると思います。
英語が苦手なもので(汗)毎回のPRに苦労するのですが、取り込まれたときはテンションあがりますね!
なにより「世界と繋がっている感覚」、味わえます。
以上です。
CodeceptionとAspectMockを使って和田卓人さんの現在時刻に依存するテストを書いてみた
TLで話題になっているCodeIQでの問題と解説はこちらです。
これであなたもテスト駆動開発マスター!?和田卓人さんがテスト駆動開発問題を解答コード使いながら解説します~現在時刻が関わるテストから、テスト容易性設計を学ぶ #tdd|CodeIQ MAGAZINE
大変丁寧な解説、とても参考になりました。
ただ、参加されている方の採用したテスティングフレームワークにCodeceptionがない!
日本のPHP開発者にCodeceptionを流行らせたいワタクシとしては看過できない事態(=チャンス)です!
ってことで、Codeceptionを使って書いてみました。
といいつつも、今回はCodeceptionがメインではなく、AspectMockがメイン。
AspectMockとは
Codeceptionの開発者の@davertさんによる「ちょーすげー(not an ordinary)」PHP向けのモッキングフレームワーク。
内部的にはGo! AOPが提供するアスペクト指向の概念を利用し、
- スタティックメソッドに対するテストダブルを作成できる
- あらゆるインスタンスメソッドに対するテストダブルを作成できる
- 処理中(on the fly)にメソッドを再定義できる
- 憶えやすいシンプルな構文
を実現しています。
AspectMockの裏側
詳しくは Understanding AspectMock に解説されていますので簡単に。
まず、GO! AOPの仕組みですが、独自のストリームラッパーを利用してincludeやrequireしたファイルをその場で解析、定義されているメソッドを抽出し、横断的関心事をウィービングするようです。
(これをPHPのみで実現しているのはすごいですね。処理重そうな感じしちゃいますが。。。)
AspectMockはこのGO! AOPの仕組みを利用し、全てのメソッドの一番最初に「Stubがあればそれ返してよ!」という処理をブッコミます。
<?php class User { function setName($name) { $this->name = $name; }
これが、、、
<?php class User { function setName($name) { if (($__am_res = __amock_before($this, __CLASS__, __FUNCTION__, array($name), false)) !== __AM_CONTINUE__) return $__am_res; $this->name = $name; }
こうなる。
アプローチ
AspectMockの仕組み上、PHPの標準関数に対するテストダブルは作成できないので、getHour
のようなメソッドを作成し、それに対するテストダブルを作成します。
言語 | テスティングフレームワーク | アプローチ | アプローチ詳細 |
---|---|---|---|
PHP5.4 | Codeception, AspectMock | 時刻ライブラリに介入とまではいかないけどむにゃむにゃ... | 標準関数のラッパーをAspectMockで stub |
といったところでしょうか.....
準備
1. Composer
以下の内容でcomposer.json
を用意してphp composer.phar install
します。
{ "require-dev": { "codeception/codeception": "1.8.*", "codeception/aspect-mock": "*" }, "autoload": { "psr-0": {"Greeter\\": "src/"} }, "config": { "bin-dir": "bin" } }
2. Codeception、AspectMockの初期化
bin/codecept bootstrap
します。
testsディレクトリ配下に必要なファイル群が作成されますので、tests/_bootstrap.php
を以下のように編集し、AspectMockを初期化します。
<?php // This is global bootstrap for autoloading $kernel = AspectMock\Kernel::getInstance(); $kernel->init();
3. クリーンアップ処理の定義
和田卓人さんも「組み込みクラス/メソッド/関数に介入する」アプローチの注意点として、以下のようにお書きでした。
このアプローチのデメリットは、強力さの裏返しです。力には責任が伴います。組み込みクラス/メソッド/関数の動きを変えるというのは、影響の大きい変更です。
(中略)
ゆえに、このアプローチを使う場合には、テスト後の始末を忘れずに行うことが重要です。テストの中で振る舞いを変えた標準クラス/メソッド/関数を元に戻しておかないと、他のテストにも影響が残ります。
tests/_helpers/CodeHelper.php
に後始末を以下のように記述します。
<?php namespace Codeception\Module; // here you can define custom functions for CodeGuy class CodeHelper extends \Codeception\Module { public function _after(\Codeception\TestCase $test) { \AspectMock\Test::clean(); } }
4. Unitテストの作成
bin/codecept genereate:test unit Greeter
を実行し、Greeterクラスに対する単体テストをtests/unit/GreeterTest.php
に作成します。
これで準備完了です。
Greeter
src/Greeter/Greeter.php
を以下のように実装しました。
<?php namespace Greeter; class Greeter { public function greet() { $hour = self::getHour(); if (5 <= $hour && $hour < 12) { return 'おはようございます'; } if (12 <= $hour && $hour < 18) { return 'こんにちは'; } return 'こんばんは'; } private static function getHour() { return date("G", time()); } }
getHour
は標準関数time
を使い、時間に変換して返します。
ここをAspectMockでstubすれば、テスタブルになります。
GreeterTest
tests/unit/GreeterTest.php
に以下のようにテストを記述しました。
<?php use AspectMock\Test as test; class GreeterTest extends \Codeception\TestCase\Test { protected $codeGuy; /** * @dataProvider greetProvider */ public function testGreet($hour, $message) { test::double('\Greeter\Greeter', ['getHour' => $hour]); $greeter = new \Greeter\Greeter(); $this->assertEquals($message, $greeter->greet()); } public function greetProvider() { return array( array(4, 'こんばんは'), array(5, 'おはようございます'), array(6, 'おはようございます'), array(11, 'おはようございます'), array(12, 'こんにちは'), array(13, 'こんにちは'), array(17, 'こんにちは'), array(18, 'こんばんは'), array(19, 'こんばんは'), ); } }
[ポイント1] CodeceptionのUnitテストではPHPUnitのメソッドを利用できる!
\Codeception\TestCase\Test
はPHPUnit_Framework_TestCase
を継承しているため、
$this->assertEquals($greet, $greeter->greet());
等、普通に使えます。
もう、さっさとPHPUnitからCodeceptionに乗り換えましょう。
[ポイント2] パラメタライズドテスト(Parameterized Test)
和田卓人さんも言及していた、PHPUnitのデータプロバイダ、Codeceptionのバージョン1.8から利用できるようになりました!
だから、さっさとPHPUnitからCodeceptionに乗り換えましょう。
[ポイント3] テストダブル
今回の本命AspectMockを使い、
test::double('\Greeter\Greeter', ['getHour' => $hour]);
で、GreeterクラスのgetHourメソッドを stub してます。
実際のテスト結果
bin/codecept run unit
します。
Codeception PHP Testing Framework v1.8.0 Powered by PHPUnit 3.7.28 by Sebastian Bergmann. Unit Tests (1) ------------------------------------------------------ Trying to test greet with data set #0 (GreeterTest::testGreet) Ok Trying to test greet with data set #1 (GreeterTest::testGreet) Ok Trying to test greet with data set #2 (GreeterTest::testGreet) Ok Trying to test greet with data set #3 (GreeterTest::testGreet) Ok Trying to test greet with data set #4 (GreeterTest::testGreet) Ok Trying to test greet with data set #5 (GreeterTest::testGreet) Ok Trying to test greet with data set #6 (GreeterTest::testGreet) Ok Trying to test greet with data set #7 (GreeterTest::testGreet) Ok Trying to test greet with data set #8 (GreeterTest::testGreet) Ok --------------------------------------------------------------------- Time: 11.3 seconds, Memory: 44.75Mb OK (9 tests, 9 assertions)
パスしました。