オープンソースこねこね

Webプログラミングなどについてあれこれ。

PHP Webアプリケーションから非同期にコマンドを実行するライブラリをつくりました。

こんなの作りました。

BackgroundProcess

Webのプロセスから重い処理を実行したいのだけど。。。

PHPCSVファイルなどから一括で大量のデータ投入を行いたいときがあります。 これをWebアプリケーションのプロセスでやろうとすると、処理時間が長いため、だいたいプロセスがタイムアウトしてエラーになってしまいます。困ったものです。

こういうとき、代わりにコマンドラインのプログラムとして実装したりするんですが、任意のタイミングで処理したい場合、コマンド実行する以上サーバにターミナルでログインできる技術者しかデータ投入作業ができなくなってしまいます。 もっとだれにでもできるように、やっぱりWebアプリケーションのインターフェースが欲しくなるのが人情です。

Webアプリから非同期にコマンドを実行する

この要件を満たす簡単な方法は、以下のようなコードでWebプロセスから外部コマンドをバックグラウンドで実行することです。

exec("php yourcommand.php > /dev/null &");

こうすることで実行コマンドは非同期にバックグラウンドで動作し、Webのプロセスはコマンドの処理内容にかかわらず、即時レスポンスを返すことができます。

ただこれだと、以下のような不満があります。

  • 外部コマンドのエラー処理が考えられていない。コマンドごとにそれぞれ内部で何らかの実装をする必要がある。
  • 実行開始したコマンドのプロセスをWebプロセス側から関与することができない。

こういうことをうまく扱うために、ジョブキューエンジンや、DBなどを使ったタスクやプロセスを管理する仕組みはすでにあると思います。 でもそこまで手間をかけたり、構成を複雑にしたくない。

ちょっとだけ発展させてみました

というわけで作ったのが、

BackgroundProcess

インストールは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の取得などを行っています。 コマンドが正常終了すると、これらのファイルは自動削除されます。

シーケンス図にすると以下のようになります。

20130812192611

最近の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

PHPUnitSeleniumの連携については公式ドキュメント(日本語訳あり)があります。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はそのFireFoxGUI環境がないサーバ上でも起動させられるようにする仮想の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を作ったので、起動スクリプトの内容などは以下を参照してください。

Chef cookbooks selenium

PHPUnitPHPUnit_Seleniumをインストールする

ようやくPHPの話になりました。Selenium ServerとPHPUnitを連携するため、PHPUnitPHPUnit_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ディレクトリにPHPUnitPHPUnit_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アプリケーションで、テストフレームワーク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サーバをインストールして起動、PHPUnitSelenium拡張を使ってテストケースを実行します。テストケースはfirefoxのアドオンであるSeleniumIDEでブラウザ操作を記録し、これをPHPUnitのコードに変換するプラグインでエクスポートして作成できました。

今後、コントローラの実装はSeleniumのテストケースでカバーされたら、ロジック部分をよりテスタビリティの高いプレーンなPHPクラスにリファクタリングしていこうかと考えています。

テストを書くと筋の悪い実装を認識できる

実際にテストを書いてみると、今まで気が付かなかったコードのよくない点が、テストが書きにくいという厳然たる事実によって、はっきりと分かるようになりました。

ここでのコードのよくない点というのは、なんらかの環境に強く依存しているコードで、たとえばサーバの現在日時とか特定のファイルパスとかです。これらがメソッド内部に埋め込まれていたりすると、非常にテストが書きづらい。このような環境依存情報をメソッドの引数とかコンストラクタとかでオブジェクト外部から渡せるように分離すればテストがぐっと書きやすくなります。

テストを書くと、プログラムを小さい単位で動作検証しながら実装できます。 そもそも今までも、開発中は動作確認のため、ちょくちょくコードを実行させながらコーディングしていました。PHPのWebアプリ開発だと動作確認はブラウザをリロードして目視することだったのですが、その目視をテストコードで自動化したわけです。 しかも、今までよりもっと小さな単位でプログラムを動作保証させながら開発していくことができる。

このテストを書くことによる「コンパクトに動作確認がとれているコードを積み重ねていく」というスタイルのコーディングが、体験してみるとすごくイイ感じなのでした。ひとつひとつの部品が綺麗に磨き上げられた上で、ものが作られていくような感覚です。

開発中のテストはよりよいコードを書くための「ツール」

私のなかでテストコードは、よいコードを書くためのツールという認識になりました。 ペンで綺麗な直線を引くのに定規を使うように、よいコードを記述するのにテストを書く。 ツールであるテストコードは「実装」と不可分な存在で、これは工数見積とかで見積書の上によくでてくる実装と分けられた「テスト」タスクとは明らかに違います。 その文脈での「テスト」は書かれたコードを対象にするの対して、テストコードは書いている最中のコードを対象にしています。

テストはシステムを保護する

開発中に書いたテストコードは、リグレッションテストとして再利用されます。 その昔、メソッドのインターフェースが変わるような修正をした場合、テストコードも書きなおさないといけないので、作業量が増える! 開発効率が落ちる! だからテストなんて書きたくない! と思ってました。

今、メソッドのインターフェースを変えたときにテストがエラーを出力することに対して、安心感さえ覚えます。それは関連してどこを修正しなければいけないかをテストが知らせてくれているわけで、システムが複雑化して自分の認識が届かないような箇所に対しても、まだすべきことがあるということを本番へのデプロイ前に教えてくれる。

そしてカバレッジをとることでコードがどれだけ手厚く保護されているのかもわかります。

というわけで

今日もテストかきかきしてますっ!

おしまい。

デプロイツールAltaxをPackagistに登録したよ。

以前作ったPHP製デプロイツール。

これをPackagistに登録しましたよ。

というわけで、composerを使っているユーザはcomposer.jsonに以下のように記述してinstallコマンドを実行すれば、プロジェクトのvendorディレクトリのbin配下にaltaxコマンドがインストールされます~。

{
    "require": {
        "kohkimakimoto/altax": "dev-master"
    }
}

求人募集をHTTPレスポンスヘッダに埋め込む

http://www.codebreak.com/

のHTMLソースに求人が書いてあって、ちょっとニヤリとしたのです。 ...あ。いいこと思いついたよ! それなら、HTTPレスポンスヘッダに求人情報を埋め込むってのはどうかな?

エンジニアなら新しいサービスやサイトを見つけると、HTTPのヘッダを確認したくなっちゃうよね!

というわけで、PHPで書くとこんなんです。

<?php
header("Recruit-Message: Are you a programmer? Why don't you work with us. Please visit our recruit page -> http://ourcompany/recruit.html");

企業のよりよい求人活動の一つとしてご利用いただければ幸いです。

LTSVをyamlぽいフォーマットで色付き表示するPHPスクリプト

LTSVをyamlぽいフォーマットで色付き表示するだけ。 以下の記事のPHP版適当実装。

Code

#!/usr/bin/env php
<?php
/*
 * ltsv viewer
 *
 * ex)
 *   cat foo.log | php ltsview.php
 */

// refereces below.
//   http://d.hatena.ne.jp/naoya/20130206/1360154312
//   http://d.hatena.ne.jp/maru_cc/20080216/1203166254

$data = file_get_contents("php://stdin");
$arr = explode("\n", $data);
foreach ($arr as $v) {
  $ltsv_arr = explode("\t", $v);

  foreach ($ltsv_arr as $ltsv_v) {
    if (strpos($ltsv_v, ":") === false) {
      continue;
    }

    list($key, $value) = explode(":", $ltsv_v, 2);
    echo pack('c',0x1B)."[1;34m".$key.pack('c',0x1B)."[0m";
    echo pack('c',0x1B)."[1;31m".":".pack('c',0x1B)."[0m";
    echo $value."\n";
  }
  echo "---\n";
}

Gistにもあげときました。