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アプリ開発だと動作確認はブラウザをリロードして目視することだったのですが、その目視をテストコードで自動化したわけです。 しかも、今までよりもっと小さな単位でプログラムを動作保証させながら開発していくことができる。
このテストを書くことによる「コンパクトに動作確認がとれているコードを積み重ねていく」というスタイルのコーディングが、体験してみるとすごくイイ感じなのでした。ひとつひとつの部品が綺麗に磨き上げられた上で、ものが作られていくような感覚です。
開発中のテストはよりよいコードを書くための「ツール」
私のなかでテストコードは、よいコードを書くためのツールという認識になりました。 ペンで綺麗な直線を引くのに定規を使うように、よいコードを記述するのにテストを書く。 ツールであるテストコードは「実装」と不可分な存在で、これは工数見積とかで見積書の上によくでてくる実装と分けられた「テスト」タスクとは明らかに違います。 その文脈での「テスト」は書かれたコードを対象にするの対して、テストコードは書いている最中のコードを対象にしています。
テストはシステムを保護する
開発中に書いたテストコードは、リグレッションテストとして再利用されます。 その昔、メソッドのインターフェースが変わるような修正をした場合、テストコードも書きなおさないといけないので、作業量が増える! 開発効率が落ちる! だからテストなんて書きたくない! と思ってました。
今、メソッドのインターフェースを変えたときにテストがエラーを出力することに対して、安心感さえ覚えます。それは関連してどこを修正しなければいけないかをテストが知らせてくれているわけで、システムが複雑化して自分の認識が届かないような箇所に対しても、まだすべきことがあるということを本番へのデプロイ前に教えてくれる。
そしてカバレッジをとることでコードがどれだけ手厚く保護されているのかもわかります。
というわけで
今日もテストかきかきしてますっ!
おしまい。
複数のmysqldプロセスを起動するinit.dスクリプト
一つのサーバで複数のmysqldプロセスを起動させたいときがあります。 この用途のために標準でmysqld_multiというコマンドがあるのですが、
このコマンドによるmysqldの起動、停止処理は即時レスポンスを返してしまって、プロセスが完全に立ち上がるのを待ってくれません。 というわけで、起動処理をちゃんと待ってレスポンスを返すような起動スクリプトが欲しかったのでinitd.dスクリプトを自作しました。
以下のスクリプトファイルを/etc/init.d/
配下に実行権限つきでおいてやってください。
設定ファイルは
/etc/my.multi.foo.cnf
/etc/my.multi.bar.cnf
のようにmy.multi.#name#.cnf
という形式で複数用意し、datadir
やpid-file
の設定などをそれぞれユニークにした設定を記述します。(MySQLのドキュメントにもあるように同じデータを複数プロセスで読み書きするのはできません)
あとは
$ sudo /etc/init.d/mysqld_multi start
を実行すれば
mysqld.foo を起動中: [ OK ]
mysqld.bar を起動中: [ OK ]
のように複数のプロセスが起動してくれます。
sqlmapをCentOSにインストールする
sqlmapはオープンソースのSQLインジェクションの脆弱性をテストするツールです。 以前テストに使用したので、インストール方法などのメモを残しておきます。
インストール on CentOS
sqlmapはPythonによる実装なのでまずPythonを入れます。
$ sudo yum install python
$ python -V
あとはGithubからsqlmapを取得するだけ
$ git clone https://github.com/sqlmapproject/sqlmap.git
$ cd sqlmap
使い方
以下のように調査したいURLを指定してsqlmap.py
を実行するだけです。
$ python sqlmap.py -u "http://localhost/post?id=a" -a
クエリストリングの値の部分(上の例だとid=aのaの部分)をsqlmapが自動でいろいろなSQLインジェクションを調査するためのパターンに置き換えて、HTTPリクエストをバンバン実行してくれます。
URLにパラメータが埋め込まれているタイプのURLであるhttp://localhost/post/id
のような場合は、アスタリスクをつけるとそこをパラメータとみなして置き換えてくれます。
$ python sqlmap.py -u "http://localhost/post/a*" -a
POSTメソッドを使いたい場合は以下のように--data
オプションでパラメータを指定するとPOSTでリクエストする
$ python sqlmap.py -u "http://localhost/post" -a --data "id=1"
脆弱性をついてファイルを作ることもできる。
$ python sqlmap.py -u "http://localhost/post?id=a" -a --file-write="/var/tmp/foo.txt" --file-dest="/var/tmp/foo.txt"
ファイルを読む。
$ python sqlmap.py -u "http://localhost/post?id=1" -a --file-read="/var/tmp/foo.txt"
コマンド実行する。
$ python sqlmap.py -u "http://localhost/post?id=a" -a --os-cmd="touch /var/tmp/foo.txt"
ヘルプの表示
$ python sqlmap.py -h
詳細なヘルプの表示
$ python sqlmap.py -hh
sqlmapはSQLインジェクションを検出すると、その脆弱性を通してDBアクセスユーザのパスワードを辞書ベースで解析します。
テスト用に脆弱性のあるsqlinjection.php
を作ったので、安全な環境でうごかして見ると、DB内部がまるっと見えてしまったりして、なかなかョッキングな体験を味わえます。
その他
以下は、SQLインジェクションでファイルの作成やコマンドを実行できてしまうメカニズムの説明資料。
- https://www.owasp.org/images/d/dc/AppsecEU09-Damele-A-G-Advanced-SQL-injection-slides.pdf
- http://www.slideshare.net/inquis/advanced-sql-injection-to-operating-system-full-control-whitepaper-4633857
要点