PHPのArrayを少し便利に扱えるクラスを書いた。
大したコード量じゃないので毎回コピペして使ってたコードなのですが、せっかくなのでテスト書いて、TravisCIとCoverallsでテストして、Packagistに登録してみました。
https://github.com/kohkimakimoto/EArray
EArray
PHPで配列にアクセスするとき存在しないキーで値を取得しようとすると、警告がでます。
<?php $arr = array("key_a" => "aaa"); echo $arr["key_b"]; // PHP Notice: Undefined index: key_b ...
で、これを避けるために事前にキーの存在をチェックする処理をいれたりするんですが、いちいちこのコードを書くのが地味に面倒なのですよね。
$arr = array("key_a" => "aaa"); echo isset($arr["key_b"]) ? $arr["key_b"] : null;
これ、多次元配列だと更に面倒。
@(アットマーク)を配列の前につけてエラー出力しないように制御する方法もありますが、単に警告を無視してることになって、どうにも「その対応は違うだろ」って気持ちになってやりたくないですし。
そんなわけで、前述のEArrayというクラスを書きました。使い方は対象の配列をコンストラクタで登録してgetメソッドを使ってアクセスする。キーが存在しない場合、デフォルトでnullを戻します。
use Kohkimakimoto\EArray\EArray; $earray = new EArray(array("foo" => "bar")); $earray->get("foo"); $earray->get("foo2", "default"); // 第二引数でキーが存在しない時に戻す値を指定できる。
多次元配列の場合は、スラッシュ区切りの記法でスマートにアクセスできます。
$earray = new EArray( array( "foo" => array( "foo2" => array( "foo3", "foo4", ), "foo2-1" => "foo5", ), "bar", "hoge", ) ); $earray->get("foo/foo2-1"); # "foo5"を戻す。 $earray->get("foo.foo2-1", null, "."); # キーにスラッシュが含まれている場合は第三引数で区切り文字を指定すればいい。
なお、ArrayAccessやIteratorインターフェースを実装しているので、普通の配列としてもオペレーションできます。 他にもREADMEにサンプルコードをいくつか書いておいたので参考にしてください。
https://github.com/kohkimakimoto/EArray/blob/master/README.md
インストール
composerを使います。composer.jsonに以下の行を追加してcomposer install
すればいいと思います。
"kohkimakimoto/earray": "1.1.*"
まあ、クラスファイル一個だけなので、ちょっと使いたい人は以下のコードをコピペしてネームスペース定義を変えるとかしてもいいかもしれません。
https://raw.github.com/kohkimakimoto/EArray/master/src/Kohkimakimoto/EArray/EArray.php
ちなみにEArrayのEはExtendedの意味です。
デプロイツールAltaxをバージョン2に更新しました
Capistrano的なPHP製デプロイツールAltaxをバージョン1→2としてアップデートしました。
https://github.com/kohkimakimoto/altax
このソフトウェアはPHPで記述したタスクを、複数のリモートサーバに対して実行します。 サーバ接続には鍵認証のSSHを使用し、並列に実行します。 動作に必要なファイルは1つだけで、PHPの実行環境があれば簡単に導入できます。
Version1からの変更点
- 実装をSymfony Componentsを使って大幅に変更、再構成。
- pharによる実行ファイル形式の配布をサポート。
- デフォルトの読み込み設定ファイルのパスを変更。
- クラスベースのタスク定義をサポート。
- ログ出力機能を追加。
- SSH接続の実装部分をsshコマンドからphpseclibに変更。
インストール
ドキュメントはまだ更新してないので、ざっくり最初の使い方だけ記述しておきます。実行にはPHP5.3以上が必要です。またWindowsでは多分動きません。
インストールは以下のコマンドを実行するだけです。
$ curl https://raw.github.com/kohkimakimoto/altax/master/installer.sh | sudo bash -s system v2
/usr/local/bin/altax
に実行ファイルがインストールされます。
手動で好きな場所にインストールしたい場合は、以下のように直接altax.phar
を持ってきて配置すればいいでしょう。
$ wget https://github.com/kohkimakimoto/altax/raw/master/altax.phar
$ chmod 755 altax.phar
$ mv altax.phar /path/to/install_dir/altax
とりあえず実行する
なにも付けずにコマンドを叩くと、バージョン情報やサブコマンド一覧が表示されます
$ altax
設定ファイルを作成する
$ altax init
initコマンドを実行するとカレントディレクトリに以下のような構造の.altax
ディレクトリを作成します。
.altax/
config.php # メインの設定ファイル。
hosts.php # 接続先ホストを記述する用のファイル
tasks/ # タスク定義を置くディレクトリ
hellow.php.sample # サンプルのタスク定義。
必須なのはconfig.php
だけで、その他のファイルは必要に応じてconfig.php
から読み込んで使用します(今回は使いません)。
config.phpはコメントアウトされているところを除くと以下のようになっています。
host('127.0.0.1', array('web', 'localhost'));
desc('This is a sample task.');
task('sample',array('roles' => 'web'), function($host, $args){
run('echo Hellow World!');
});
host関数で接続先ホストの設定、taskで実行タスクを定義しています。このサンプルではローカルホストに接続しにいきます。 接続先ホストはパスワードなしの鍵認証でSSHできるようにしておく必要があります。
再びaltax
コマンドをサブコマンドなしで実行します。
$ altax
Available commands:
config Show configurations
help Displays help for a command
init Create default configuration directory in the current directory
list Lists commands
sample This is a sample task.
Available commandsにsampleタスクが表示されます。実行してみましょう。
$ altax sample
Altax version 2.1.0
Starting altax process
- Starting task sample
Found 1 target hosts: 127.0.0.1
- Running sample at 127.0.0.1
Hellow World!
Completed task sample
以上のようにHellow World!がechoコマンドで実行されます。
詳細な使い方について
今回はここまでです。詳細は後日、ドキュメントかブログにまとめようと思っています。
メモ) Symfony Componentsが激しく便利な件
今回コマンドラインのプログラムを書き直したわけですが、その際に利用したSymfony Componentsが激しく便利でした。
あたりを使ったわけですが、 今までコマンドラインオプションをパースする処理をgetopt関数使ってシコシコ書いていたりしたのが、すんごく簡単に書けるようになります。 出力メッセージに色つけたり、ディレクトリからファイル一覧を持ってきたりとかもすんごく楽。
みんなも使ったほうがいいですヽ(^o^)丿。
メモ) だれか知ってたら教えて
SSHでコマンド実行が正常にできることのテストをtravis-ciでやるのってどうすんだと。 わからん。とりあえず、このあとCapistranoのテストコード見に行ってきますよ、と。
PHP Webアプリケーションから非同期にコマンドを実行するライブラリをつくりました。
こんなの作りました。
Webのプロセスから重い処理を実行したいのだけど。。。
PHPでCSVファイルなどから一括で大量のデータ投入を行いたいときがあります。 これをWebアプリケーションのプロセスでやろうとすると、処理時間が長いため、だいたいプロセスがタイムアウトしてエラーになってしまいます。困ったものです。
こういうとき、代わりにコマンドラインのプログラムとして実装したりするんですが、任意のタイミングで処理したい場合、コマンド実行する以上サーバにターミナルでログインできる技術者しかデータ投入作業ができなくなってしまいます。 もっとだれにでもできるように、やっぱりWebアプリケーションのインターフェースが欲しくなるのが人情です。
Webアプリから非同期にコマンドを実行する
この要件を満たす簡単な方法は、以下のようなコードでWebプロセスから外部コマンドをバックグラウンドで実行することです。
exec("php yourcommand.php > /dev/null &");
こうすることで実行コマンドは非同期にバックグラウンドで動作し、Webのプロセスはコマンドの処理内容にかかわらず、即時レスポンスを返すことができます。
ただこれだと、以下のような不満があります。
- 外部コマンドのエラー処理が考えられていない。コマンドごとにそれぞれ内部で何らかの実装をする必要がある。
- 実行開始したコマンドのプロセスをWebプロセス側から関与することができない。
こういうことをうまく扱うために、ジョブキューエンジンや、DBなどを使ったタスクやプロセスを管理する仕組みはすでにあると思います。 でもそこまで手間をかけたり、構成を複雑にしたくない。
ちょっとだけ発展させてみました
というわけで作ったのが、
インストールはComposerで以下のようにcomposer.json
を作成して
{
"require": {
"kohkimakimoto/background-process": "1.1.*"
}
}
インストールコマンドを実行。
$ curl -s http://getcomposer.org/installer | php
$ php composer.phar install
使い方は、実行したいコマンドをコンストラクタに指定してオブジェクトを生成した後runメソッドを実行するだけです。
use Kohkimakimoto\BackgroundProcess\BackgroundProcess;
// Creates instance and set command string to run at the background.
$process = new BackgroundProcess("ls -l > /tmp/test.txt");
// Runs command, and it returns immediately.
$process->run();
// Get key identified the process.
$key = $process->key();
$process->key()
でプロセスを識別する一意キーが取得できます。
これはあとで実行したコマンドが実行中であるかなどを確認するのに以下のように使います。
use Kohkimakimoto\BackgroundProcess\BackgroundProcess;
$manager = new BackgroundProcessManager();
$process = $manager->loadProcess($key);
// If a process specified by the key dosen't exist, loadProcess method returns null.
if (!$process) {
echo "Not working process $key";
} else {
$meta = $process->getMeta();
echo $meta['created_at']; // (ex 2013-01-01 10:00:20
echo $meta['pid']; // (ex 1234
}
また、コマンドがエラーになった場合デフォルトで下のパスのログファイルにエラーログ(標準エラー出力)を出力します。
/tmp/php/background_process/err.log
内部の作り
前述したように、手軽に使えるようにしたかったのでDBなどは使っていません。
BackgroundProcessはrunメソッドを実行すると、デフォルトで/tmp/php/background_process/
配下にコマンドごとに、PHPファイルとJSONファイルを出力します。
PHPファイルはコンストラクタで指定したコマンドの実行を行うスクリプトで、JSONファイルはプロセスIDなどのメタデータを保持しています。
これらのファイルを利用してコマンド実行やエラー処理、プロセスIDの取得などを行っています。
コマンドが正常終了すると、これらのファイルは自動削除されます。
シーケンス図にすると以下のようになります。
最近のPHPWebアプリケーション開発環境
というタイトルでスライドを作りました。
http://kohkimakimoto.github.io/MySlide/slides/phpenvironment/index.html
内容はともかく、スライド作成のために利用したreveal.jsがすごくいいです。HTMLベースのかっこいいスライドを簡単に作れます。スライドの内容は標準でMarkdownで書けますし。やっぱMarkdownでしょまーくだうん。 そして、これを最近なんやかんやでちょくちょく触っていたJekyll上で動作確認し、GithubPagesでホスティングする。 これもいい感じです。
Githubリポジトリ
https://github.com/kohkimakimoto/MySlide
スライドの内容
- VirtualBox
- Chef
- Composer
- LibMigration
- Altax
- Ansible
- Travis CI
- coveralls
- その他いろいろ
PHP WebアプリケーションのテストにSeleniumを使う - その2:PHPからテストを実行する
前回のお話
PHP WebアプリケーションのテストにSeleniumを使う - その1:SeleniumIDE編
に引き続きSeleniumについて。今回はSelenium Serverを立てて、PHPUnitと連携させてみます。 これができるとユニットテストでは難しいMVCのコントローラのテストや、エンドツーエンドテストができて、 クラス全体の構成を大きく変えるような粒度の大きいリファクタリングを安全に行うことができるようになります。
なお、以下のオペレーションはすべてCentOS6上を想定しています。
Selenium Server
PHPUnitとSeleniumの連携については公式ドキュメント(日本語訳あり)があります。Selenium Serverのインストール方法もここに記載されています。
http://phpunit.de/manual/current/ja/selenium.html
前回のSeleniumIDEはFireFoxのアドオンでしたが、Selenium ServerはJavaで書かれたサーバプログラムで、起動するとサーバプロセスとしてシステムに常駐します。このサーバに対してテストコマンドをリクエスト(プロトコルはHTTP)すると、Selenium Serverがブラウザを立ち上げて、テストオペレーションを実行してくれる仕組みになっています。
インストールは上記のサイトに書いてあるようにselenium-server-standalone-2.xx.x.jarをダウンロードして適当なディレクトリにおくだけです。 また実行にJavaが必要です。Javaをyumでインストールする場合は以下のようにしましょう。
# yum install java-1.7.0-openjdk
XvfbとFireFox
さて、Selenium Serverがテストを実行するにあたって他に次の2つが必要です。
FireFoxはテストを実行するブラウザで、XvfbはそのFireFoxをGUI環境がないサーバ上でも起動させられるようにする仮想のGUI環境というわけであります。
これらはyumによって以下のコマンドでインストールできます。
# yum install xorg-x11-server-Xvfb
# yum install firefox
XvfbとSelenium Serverを起動
XvfbとSelenium Serverはサーバプロセスなので、それぞれ起動して実行中にしておきます。
Xvfb
$ /usr/bin/Xvfb :99 -screen 0 1024x768x8
Selenium Server
$ java -jar selenium-server-standalone-2.33.0.jar -port 4444
ただ、私はサーバプロセスは/etc/init.d/
配下の起動スクリプトでコントロールしたいと考えていたので、直接上記のコマンドは叩かず、起動スクリプトを書いてそれを実行するようにしました。この場合以下のような起動方法になります。
# /etc/init.d/xvfb start
# /etc/init.d/selenium start
インストールを含めてchefのcookbookを作ったので、起動スクリプトの内容などは以下を参照してください。
PHPUnitとPHPUnit_Seleniumをインストールする
ようやくPHPの話になりました。Selenium ServerとPHPUnitを連携するため、PHPUnitとPHPUnit_Seleniumをインストールします。 これはComposerでのインストールに対応しているのでComposerを使います。
Composerについてはここで詳細な解説しませんが、PHPのモジュール管理の仕組みです。RubyでいうところのBundlerですね。
composer.json
ファイルを以下のように作成して
{
"require": {
},
"require-dev": {
"phpunit/phpunit": "3.*",
"phpunit/phpunit-selenium": ">=1.3.1"
}
}
composer install
コマンドを実行します。
$ curl -s http://getcomposer.org/installer | php
$ php composer.phar install
これでプロジェクトのカレントディレクトリ配下のvendorディレクトリにPHPUnitとPHPUnit_Seleniumがインストールされます。
テストケースを作成する
http://phpunit.de/manual/current/ja/selenium.html
にサンプルのテストケースがあるのでそれを実行してみましょう。URLなどは適宜読み替えてください。
<?php
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class WebTest extends PHPUnit_Extensions_SeleniumTestCase
{
protected function setUp()
{
$this->setBrowser('*firefox');
$this->setBrowserUrl('http://www.example.com/');
}
public function testTitle()
{
$this->open('http://www.example.com/');
$this->assertTitle('Example WWW Page');
}
}
実行は以下のようにコマンドを打ちます。
$ php vendor/bin/phpunit path/to/WebTest.php
Selenium Serverの標準出力(上記の起動スクリプトの場合は/var/log/selenium.log)にFireFoxの起動情報やテストコマンドが出力されて、Selenium Serverが動作していることが確認できると思います。
SeleniumIDEでテストケースを作る
上記のようなPHPをすべて手で書くのはきびしいです。 ところで前回解説したSeleniumIDEには、ブラウザ操作をSeleniumのテストケースとして記録してくれる機能がありました。 さらに以下のアドオンをFireFoxにインストールすると、この記録したテストケースを上記のようなPHPUnit用のテストケースに変換して出力することができます。
https://addons.mozilla.org/ja/firefox/addon/selenium-ide-php-formatters/
[ファイル]→[テストケースをエクスポート]→[PHP(PHPUnit)]で利用できるテストケースを出力することができます。
長くなって気力がつきたのでまた次回
次回はテスト時の初期処理、カバレッジレポートの取得などについて書こうかと思います。
気力が回復すれば書きます。たぶん。
PHP WebアプリケーションのテストにSeleniumを使う - その1:SeleniumIDE編
前回テストについての記事を書いたのですが、今回はその時に使用したSeleniumについて。大変便利なのです、このツールは。
Selenium
Seleniumはブラウザでのテストを自動化するツールです。 普段Webアプリケーションのテストでやっているであろう「ブラウザを開く」「リンクをクリックする」「フォームに値を入力してボタンを押す」などのオペレーションをSeleniumがあなたの代わりにブラウザを動作させて、自動テストしてくれるわけです。
Selenium? Selenium RC? Selenium Server? SeleniumIDE?
なお、Seleniumといってもなんかいろいろあるので、以下の記事を読んでおくといいでしょう
Selenium何とかっていうツールがやたら色々あるのはどういうわけなのか
入門 - SeleniumIDEを触る
最終的にはテストフレームワークのPHPUnitと連携させてテストするようにしたのですが、初めての人はまず入門としてSeleniumIDEを使ってみるのがオススメです。これはFireFoxのアドオンでテストケースに書いた内容を、あなたの代わりにSeleniumIDEがFireFoxをもりもり自動操作してテストしてくれるツールです。
以下のダウンロードサイトからselenium-ide-2.2.0.xpiをダウンロードします(バージョンは2013/08/08の最新。適宜読み替えてください)。インストールはFireFoxのウィンドウにダウンロードしたselenium-ide-2.2.0.xpiをドラッグアンドドロップすればOKです。
http://docs.seleniumhq.org/download/
インストールしたらFireFoxのメニューから[ツール]→[Selenium IDE]を選択します。
SeleniumIDEが開きます。
まずはテストケースを作成します。テストケースはFireFoxを実際に操作してそれを記録することで作成できます。 SeleniumIDEを開くとデフォルトでウィンドウ右にある赤いボタン(レコードボタン)がONになっているはずです。 この状態でFireFoxを操作するとSeleniumIDEは自動でコマンドをテストケースにインサートします。SeleniumIDEのウィンドウにリンクやボタンのクリックや値の入力をコマンドとして記録していくのがわかると思います。
さらに記録中に表示中のwebページ上で、右クリックするとコンテキストメニューからverifyとかassertとかの値検証用コマンドを選んで記録できる。
たとえば上の画像にある「assertTextPresent konekone.org」は、Webページ上に「konekone.org」という文字が表示されていることを検証するコマンドをテストケースに追加します。 ひと通り操作を記録したらウィンドウ右で押された状態にあるレコードボタンをクリックして記録状態を終了、解除させます。
さて実行させましょう。緑色の右矢印のボタンがテストケース実行ボタンなので、これを押します。 FireFoxがもりもり自動で動いて、テストケースを実行してくれると思います。
次回に続く
Selenium(SeleniumIDE)が何をやってくれるのかは、これで判ると思います。便利ですね。 でもこれだけだとテスト前にDBを初期化したり、キャッシュ消したりするような前処理ができないので、さらにテスト自動化を進めるのにはちょっと困ります。 カバレッジをとったりすることもできません。
次回はこのへんを解決するためにPHPUnitとSeleniumを連携させてみます。というわけで次回に続きます。たぶん。
というかこの時点ではPHP関係無かった。。。
テストがないとコードが書けない身体になってしまった。。。くやしいっ!
これまでテストコードなんてほとんど書いてこなかったのですが、この数日でいつのまにかテストを書くようになっていました。ずっと毛嫌いしていたのですが。今回はそんな過去と決別し、テスト環境を用意した過程やテストへの雑感などをつらつらと書き出したいと思います。なおテスト対象はPHPによるWebアプリケーションで、テストフレームワークはPHPUnitを使ってます。
なぜテストを書いてこなかったのか?
その昔、Javaの業務システムのプログラマをやっていたとき、プロジェクトでJUnitを使って少しだけテストコードを書いたことがありました。 この時の体験があまり良いものではありませんでした。そのプロジェクトのテストでは以下のような不満がありました。
- 一度書いたテストがメンテされておらず、古いテストケースのエラーが放置されていた。
- DBにアクセスする重要なクラスのテストは手間がかかって皆書かなかった。
- ユーティリティクラスのような、もともとバグが混入する可能性の少ない箇所だけテストがあった。
- 簡単なテストだけが書かれていて、テストの有用性が体感できなかった。
- 当時はCIのような、継続的にテストをプロジェクトとして回す仕組みもなかった。
このプロジェクトではテストする以前に、アプリのポータビリティやテストのしやすさなどが考慮されず、テストコードを書くための環境が整備されていませんでした。 このためテストの作成は、書くのが大変、有用性が実感できない、メンテできない、でも(仕事なので)書かなくてはいけない、というネガティブで苦痛な作業でしかありませんでした。
少しずつテスト環境を整備
今回はテストを書きつつ、地道にテストに適した環境を整えていきました。具体的には以下のような作業を行ないました。
- ComposerでPHPUnitをインストール。
- phpunit.xml.distにカバレッジレポートやテストから除外するファイル、初期化処理などの設定を記述。
- テスト起動時にアプリのフレームワークをロードしコンテキストを初期化するスクリプトを作成。
- テスト用DBの作成。
- テスト用DBのスキーマ定義を本番のものと一元管理するためにMigrationツールを自作。
- テスト環境構築用にchefのcookbookを修正。
- 機能テストのためにSeleniumをインストール。chefのcookbookを作成。
この段階では一つテストを書こうとしては詰まって、その都度環境設定をいじったり、ツールを書いたり、PHPUnitのドキュメント読んだりして、作業の支障になる環境要因を1つずつ取り除いて行きました。テストを書くという本来の作業から脇道にそれまくりで地味に大変でした。
テストを書く
環境が整っていくにつれてだんだんとテストが書きやすくなっていきました。 アプリのORMはPropelなので、DBテスト時のデータ初期化はPropelのfixtureを使いました。
Seleniumを使う
MVCのコントローラにロジックが直書きされている箇所はユニットテストが難しいので、Seleniumを使いました。 Seleniumサーバをインストールして起動、PHPUnitのSelenium拡張を使ってテストケースを実行します。テストケースはfirefoxのアドオンであるSeleniumIDEでブラウザ操作を記録し、これをPHPUnitのコードに変換するプラグインでエクスポートして作成できました。
今後、コントローラの実装はSeleniumのテストケースでカバーされたら、ロジック部分をよりテスタビリティの高いプレーンなPHPクラスにリファクタリングしていこうかと考えています。
テストを書くと筋の悪い実装を認識できる
実際にテストを書いてみると、今まで気が付かなかったコードのよくない点が、テストが書きにくいという厳然たる事実によって、はっきりと分かるようになりました。
ここでのコードのよくない点というのは、なんらかの環境に強く依存しているコードで、たとえばサーバの現在日時とか特定のファイルパスとかです。これらがメソッド内部に埋め込まれていたりすると、非常にテストが書きづらい。このような環境依存情報をメソッドの引数とかコンストラクタとかでオブジェクト外部から渡せるように分離すればテストがぐっと書きやすくなります。
テストを書くと、プログラムを小さい単位で動作検証しながら実装できます。 そもそも今までも、開発中は動作確認のため、ちょくちょくコードを実行させながらコーディングしていました。PHPのWebアプリ開発だと動作確認はブラウザをリロードして目視することだったのですが、その目視をテストコードで自動化したわけです。 しかも、今までよりもっと小さな単位でプログラムを動作保証させながら開発していくことができる。
このテストを書くことによる「コンパクトに動作確認がとれているコードを積み重ねていく」というスタイルのコーディングが、体験してみるとすごくイイ感じなのでした。ひとつひとつの部品が綺麗に磨き上げられた上で、ものが作られていくような感覚です。
開発中のテストはよりよいコードを書くための「ツール」
私のなかでテストコードは、よいコードを書くためのツールという認識になりました。 ペンで綺麗な直線を引くのに定規を使うように、よいコードを記述するのにテストを書く。 ツールであるテストコードは「実装」と不可分な存在で、これは工数見積とかで見積書の上によくでてくる実装と分けられた「テスト」タスクとは明らかに違います。 その文脈での「テスト」は書かれたコードを対象にするの対して、テストコードは書いている最中のコードを対象にしています。
テストはシステムを保護する
開発中に書いたテストコードは、リグレッションテストとして再利用されます。 その昔、メソッドのインターフェースが変わるような修正をした場合、テストコードも書きなおさないといけないので、作業量が増える! 開発効率が落ちる! だからテストなんて書きたくない! と思ってました。
今、メソッドのインターフェースを変えたときにテストがエラーを出力することに対して、安心感さえ覚えます。それは関連してどこを修正しなければいけないかをテストが知らせてくれているわけで、システムが複雑化して自分の認識が届かないような箇所に対しても、まだすべきことがあるということを本番へのデプロイ前に教えてくれる。
そしてカバレッジをとることでコードがどれだけ手厚く保護されているのかもわかります。
というわけで
今日もテストかきかきしてますっ!
おしまい。