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

Codeceptionのドキュメントの翻訳をはじめました。

Codeception

日本のPHPerにもっとCodeceptionを知ってもらうため、ついに?立ち上がりました。

タイトルのとおり、Codeceptionのドキュメントの翻訳をはじめました。

piccagliani/Codeception.docs.ja_JP · GitHub

  • 日本語ドキュメント置き場

Codeception.docs.ja


以前、ポルトガル語に翻訳された方がいて、その方とメイン開発者の@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テストをする

Docker Jenkins Codeception Selenium

前回のエントリの続きです。

今回は単純なアプリのテストなので、Dockerfileもテスト手順もシンプルでした。
より複雑な手順が必要となる、Seleniumを利用したWEBアプリのE2Eテストなどもこの方式でできるのかどうか、引き続き実験してみます。

ということで実験しました。

目次

  • 前提環境
  • 実際の手順
    1. WEBアプリを準備する
    2. Dockerコンテナ上でWEBアプリケーションを公開する
    3. Seleniumコンテナを導入、起動する
    4. Codeception+WebDriverの設定をする
    5. テスト実行用のスクリプトを作成する
    6. Jenkinsにジョブを作成し、ビルドする
  • 今後の課題など

前提環境

前回同様

OS
Windows 8:
Virtual Box
4.3.20
Vagran
1.7.2

実際の手順

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に記述します。
少し悩んだ点は、ApacheMySQLなどの設定をどのように行えばよいか、ということ。
選択肢として、以下を考えました。

  • 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にジョブを作成し、ビルドする

前回と基本的には同じです。

ジョブ名

リポジトリの名前に合わせました。

ジョブの形式

「フリースタイル・プロジェクトのビルド」を選択。

ソースコード管理

Gitを選択し、リポジトリのURL、資格情報を設定。

ビルド手順

「シェルの実行」を選択し、以下を記述。ポートのマッピングオプションを追加しています。

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する

CI Docker Jenkins

私が属する組織では受託開発がメインでして、サーバーやミドルウェアなどの要件を我々が自由に決定できないケースもしばしば。
基本はPHPなのですが、案件によってバージョンが異なったり、近頃ではNodeの案件なども出てきました。

長いこと「CIサーバを導入したい」と思いつつもアプリ毎にCIサーバを立ち上げるのもメンドイコスト等の関係で現実的ではないので、頭を悩ませていました。
そんな中、Dockerを使って解決している先人がいましたので、早速、試してみました。

目次

  • 前提環境
  • 先人の知恵まとめ
  • 実際の手順
    1. VagrantでJenkins + Dockerのサーバーを用意する
    2. デモアプリ(PHP, Node)を用意する
    3. Dockerファイルを作成する
    4. テスト実行用のスクリプトを作成する
    5. Jenkinsにジョブを作成し、ビルドする
  • 今後の課題など

長いです。。。

前提環境

OS
Windows 8
Virtual Box
4.3.20
Vagrant
1.7.2

先人の知恵まとめ

いろいろと調べていたところ、以下のようなポイントがあることがわかりました。

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が正常に動作していたら、以下を行っておきます。

  • Git Pluginのインストール
  • JenkinsがGitリポジトリにアクセスするためのSSHキーの設定

2. デモアプリ(PHP, Node)を用意する

ごくごく簡単なFizzBuzzをテストするアプリをPHP, Nodeのそれぞれで作成しました。

PHP

piccagliani/php-fizzbuzz · GitHub

テストはCodeceptionで記述しました。

Node

piccagliani/node-fizzbuzz · GitHub

テストはmochaで記述しました。

3. Dockerファイルを作成する

$APPROOT/.docker/Dockerfileをそれぞれに作成しました。

PHP

CentOS + PHP + Composerとします。

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にジョブを作成し、ビルドする

それぞれのアプリ用のジョブを以下のように作成しました。

ジョブ名

リポジトリの名前に合わせました。

ジョブの形式

「フリースタイル・プロジェクトのビルド」を選択。

ソースコード管理

Gitを選択し、リポジトリのURL、資格情報を設定。

ビルド手順

「シェルの実行」を選択し、以下を記述。

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サーバーを導入できそうです。

マニュアルには載っていないCodeceptionのコツ/TIPS

Codeception PHP

バージョン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

他にCSSセレクタをシンプルにするため、

  • フォームであればラベルで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にバグ修正をプルリました。

PHP 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');
Submitされない
<?php
$I->click('input[type=image]');
$I->click("input[@type='image']");

実際のPRはこちら

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がコントローラに渡されませんでした。

実際のPRはこちら


2つのPRは無事にマージされたので、おそらく1.8.5にてリリースされると思います。

英語が苦手なもので(汗)毎回のPRに苦労するのですが、取り込まれたときはテンションあがりますね!
なにより「世界と繋がっている感覚」、味わえます。

以上です。

CodeceptionとAspectMockを使って和田卓人さんの現在時刻に依存するテストを書いてみた

PHP 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\TestPHPUnit_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)

パスしました。

最後に

  • AspectMock、初めて使用しましたが「テストできぬものなどない」という感じにさせてくれますね。
  • Codeceptionとの相性も、同じ開発者だけあってバッチリそうです。
  • 今回は「現在時刻」という課題でしたが、開発者の手の届かない外部APIを利用している箇所のテスト等、より簡単に書けそうですね。
  • はじめに書きましたが和田卓人さんの記事はコード付かつとても丁寧な解説で、言語問わず読む価値ありです。
  • 今回のソース一式はGitHubにあります。