オープンソースこねこね

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

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アプリケーションのテストに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]を選択します。

f:id:kohkimakimoto:20130808120217j:plain

SeleniumIDEが開きます。

まずはテストケースを作成します。テストケースはFireFoxを実際に操作してそれを記録することで作成できます。 SeleniumIDEを開くとデフォルトでウィンドウ右にある赤いボタン(レコードボタン)がONになっているはずです。 この状態でFireFoxを操作するとSeleniumIDEは自動でコマンドをテストケースにインサートします。SeleniumIDEのウィンドウにリンクやボタンのクリックや値の入力をコマンドとして記録していくのがわかると思います。

さらに記録中に表示中のwebページ上で、右クリックするとコンテキストメニューからverifyとかassertとかの値検証用コマンドを選んで記録できる。

f:id:kohkimakimoto:20130808121110j:plain

たとえば上の画像にある「assertTextPresent konekone.org」は、Webページ上に「konekone.org」という文字が表示されていることを検証するコマンドをテストケースに追加します。 ひと通り操作を記録したらウィンドウ右で押された状態にあるレコードボタンをクリックして記録状態を終了、解除させます。

さて実行させましょう。緑色の右矢印のボタンがテストケース実行ボタンなので、これを押します。 FireFoxがもりもり自動で動いて、テストケースを実行してくれると思います。

次回に続く

Selenium(SeleniumIDE)が何をやってくれるのかは、これで判ると思います。便利ですね。 でもこれだけだとテスト前にDBを初期化したり、キャッシュ消したりするような前処理ができないので、さらにテスト自動化を進めるのにはちょっと困ります。 カバレッジをとったりすることもできません。

次回はこのへんを解決するためにPHPUnitSeleniumを連携させてみます。というわけで次回に続きます。たぶん。

というかこの時点では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サーバをインストールして起動、PHPUnitSelenium拡張を使ってテストケースを実行します。テストケースはfirefoxのアドオンであるSeleniumIDEでブラウザ操作を記録し、これをPHPUnitのコードに変換するプラグインでエクスポートして作成できました。

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

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

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

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

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

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

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

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

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

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

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

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

というわけで

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

おしまい。

複数のmysqldプロセスを起動するinit.dスクリプト

一つのサーバで複数のmysqldプロセスを起動させたいときがあります。 この用途のために標準でmysqld_multiというコマンドがあるのですが、

mysqld_multi — 複数のMySQL サーバ管理

このコマンドによるmysqldの起動、停止処理は即時レスポンスを返してしまって、プロセスが完全に立ち上がるのを待ってくれません。 というわけで、起動処理をちゃんと待ってレスポンスを返すような起動スクリプトが欲しかったのでinitd.dスクリプトを自作しました。

以下のスクリプトファイルを/etc/init.d/配下に実行権限つきでおいてやってください。

設定ファイルは

  • /etc/my.multi.foo.cnf
  • /etc/my.multi.bar.cnf

のようにmy.multi.#name#.cnfという形式で複数用意し、datadirpid-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インジェクションでファイルの作成やコマンドを実行できてしまうメカニズムの説明資料。

要点

  • DBにはselect文の結果をファイルに出力する機能があって(INTO DUMPFILEやlo_exportというコマンド)それを利用して、ファイルを任意の場所に生成できる。
  • DBにはUDF(user-defined function)というSQL内で使える組み込み関数をユーザで拡張できる仕組みがあり、上記のファイル生成の仕組みを使って「外部コマンドを実行するSQL関数」作って、コマンドを実行する。

Seleniumのchef - cookbookを作りました

chef-cookbooks-selenium

使い方

仮想フレームバッファ(xvfb)と、seleniumサーバを以下のコマンドで起動

# /etc/init.d/xvfb start
# /etc/init.d/selenium start

起動後、seleniumのテストケースを適宜実行すればいいと思います。

なお、seleniumサーバは起動してもportが開くまでにやや時間がかかるので、ポート4444をListenするまで待ってから応答を返すようにしてあります。