こんにちは、コネヒトでPHP書いたりしています金城(@o0h_)ともうします。烏龍茶が好きです。
(テスト自動化おじさんの弊社内におけるイメージ)
突然ですが!
皆さんも、自分の実装についていまいち自信が持てない時はございませぬか。
「あの実装・・頭をよぎる不安・・眠れない夜・・・・」そんな気分で毎日を過ごしていませんか?
「誰かに許して欲しい、安心をください」。
そんな時はテストを書きますよね。緑色が好きな人、この界隈には私だけではないはずです!
ただ、どうしても検査しづらいな〜どうやって試せば良いかな〜と悩ましい時もあるのではないかと。
「エンドユーザーが直接触るのってブラウザでしょ、だったら実際にブラウザを使った操作が滞りなく可能なら問題ないよね・・」とか、考えたことありませんか?
ここで闇堕ちしたプログラマは、ぽちぽちとhttp://localhostを開いたGoogle Chromeをマウスで弄って、束の間の平穏を得るのです。(身に覚えがあります)
ただ、これって恐ろしく面倒くさいし不安定だし絶対に毎回やらない手順です。
誰か勝手にやってくれ!
それ、Seleniumでできるよ
「ブラウザの操作をプログラマブルにしたい、しかもPHPで書きたい」。
それを実現する技術がございます。
百聞は一見にしかず、ということで「PHPがブラウザを動かしているサマ」を実際にご覧ください。
最初にCakePHPのテストコマンドを実行させた以降は、キーボードもマウスも一切触っていません。こういった、「ページを開いて→指定した要素をクリックして→フォームに値を入力して→送信ボタンを押して→フォームの送信結果をみる」という操作及び検査ができるのです。
しかも、非常にシンプルかつ短い行数のコードで実現で!!
実際に上記動画にて叩いたコードを参照しながら、ご紹介したいと思います。
その際に用いるのが、「Selenium」と「php-webdriver」です。
この分野はRubyやnodeJSによるバインディングが有名なような気もしますが、PHPによる実装もFacebookによって行われているものが存在します。
この記事はどんな人向け?
- Seleniumって何? / php-webdriverって何?という人
- CakePHPを使っていて、フォームの動作確認とかいちいち面倒くせぇええ!と思っている人
- CakePHPでのテスト(PHPUnit)について使い方が分かる、あるいは実際に書いたことがある人
Selenium / php-webdriverって何?
Selenium せれにうむ
Selenium - Web Browser Automation http://www.seleniumhq.org/
そもそも・・・Seleniumって何ですか?という方もいるかと思いますが、ググって見ると意外と端的に「何ができる」と紹介している記事ってないものですね。
極端に端折った言い方をすれば、
- プログラムを介してブラウザを操作できる(URLを指定してページを開いたり、ドキュメント中の要素をクリックしたり)
- それってつまり、JavaScriptとか外部リソースのDLとかiframeとかも完全に動作するって事!
- ↑の操作の結果を検査できる
- クリックしたらAPI叩いて成功したら「hogehoge」って表示したいんだけど・・・できてるかな?とか
などといった事(の自動化)が実現できます
Selenium自体はJavaのツールなので、これを利用するための色々な言語でのバインディングがあります。
もう少し厳密に言うと、「Selenium Standalone Serverを立ち上げて」「各言語の実装から、そのAPIを叩く」といった構造です。
では、PHPではどのようにして扱うのでしょうか?
php-webdriver ぴーえいちぴー・うぇぶどらいばー
facebook/php-webdriver https://github.com/facebook/php-webdriver
いくつか実装が存在していて、またPHPUnitのプラグインも存在するのですが、私たちのチームではFacebookによるPHP Webdriverを利用しています。単純にテスト目的で用いるならPHPUnitのプラグインで十分かも知れませんが、非テスト以外のツールやユーティリティを実装する際にも活用がしたく、あえてこちらを好んでいる形です。
コードとか
今回はCakePHPのテストケースにSeleniumによる検査を取り込む、というテーマでの実装を行いました。
デモアプリとして、「Facebookログインしてユーザー情報を表示する」というものです。
UsersController::login()
- ログイン前のページ。ログインボタンがある
UsersController::login_callback()
- Facebookログイン後のコールバック先、認証を介してユーザー情報を表示する
だけの至ってシンプルなものになります。
コレに対して、先のデモ動画のような振る舞いをさせるために実装した「テストコード」の全容が下記です。
これだけ!具体的に見て行きましょう。
use Facebook\WebDriver as FbDriver; use Facebook\WebDriver\Remote as FbRemote;
長ったらしいので名前空間のエイリアスを設定しています。
$host = 'http://localhost:4444/wd/hub'; $driver = FbRemote\RemoteWebDriver::create($host, FbRemote\DesiredCapabilities::chrome());
ブラウザ(ここではChrome)を立ち上げます。半分おまじないみたいなもの
$driver->get('http://localhost:8080');
アプリケーションのトップページを開きます($driver->get()
に任意のURLを渡す)。
今回は 8080番で立ち上げているので、このような記述に。
$driver->findElement(FbDriver\WebDriverBy::linkText('click to login!'))->click();
開いたページ中から
- 「click to login!」と書かれているリンク(
FbDriver\WebDriverBy::linkText()
) - を探し出して(
$driver->findElement()
) - クリックする(
->click()
)
これでFacebookに遷移します。
$driver->wait(300, 500)->until( FbDriver\WebDriverExpectedCondition::presenceOfElementLocated(FbDriver\WebDriverBy::cssSelector('#email')) );
ページ遷移をした際に、読み込みが完了するのを待ちたいですよね。それが保証されないと、「指定要素をクリック」などの操作が不可能です(Webdriver的には、存在しない要素に対する操作は例外を投げます)。
この「$driver->wait()
」はまさに「準備ができるのを待つ」ためのものです!(嬉しい)
ここでは
- 最大300秒待つ、その間500msでリトライする(単位注意!! → 参考)
- 待つのは「
#email
という要素が読み込まれ、存在する」という状態
を保証しています。すなわち、「Facebookのログインフォームが読み込まれている」状態になりました。
$driver->findElement( FbDriver\WebDriverBy::cssSelector('#email') )->click(); $driver->getKeyboard()->sendKeys(FB_TEST_USER_EMAIL); $driver->findElement( FbDriver\WebDriverBy::cssSelector('#pass') )->click(); $driver->getKeyboard()->sendKeys(FB_TEST_USER_PASS); $driver->findElement( FbDriver\WebDriverBy::cssSelector('#loginbutton input[type=submit]') )->click();
emailとpassを入力し、ログインボタンをクリック。
ログインに成功すると、「ユーザー情報表示ページ(login_callback
)」に戻ります。
$testUserFbId = '146765462327498'; $this->assertEquals( $testUserFbId, $driver->findElement(FbDriver\WebDriverBy::cssSelector('#id'))->getText(), 'ログイン後、適切にユーザーのFacebookIDが表示されている' );
$driver->findElement($hoge)->getText()
で、選択したDOMのinnerTextを取得します。これに対し、通常通りstringのアサーションをかけ、「ログインして情報を正常に取得できている」ことを検査しています。
$driver->close();
ブラウザを閉じます。お疲れ様でした!
以上になります。こんなところで、「いかに簡潔に、強力なブラウザテストが実現できるか」という片鱗を感じていただけたのではないかと思います。 グッバイ・目視!!!!
おわりに
いかがだったでしょうか!
特に別プロジェクトとの連携等で、どうしてもアプリ外のサイトをいじらなくてはいけない!という時には、やはりブラウザ上での動作をとっておくのが安心感が保てるし検証コードの実装コストも抑えやすいかな、と実感しています。
領域的にはUnitTestではないですよね。実際、振る舞いテストやらシナリオ書いたりして〜とかいった手法と相性が良いのかな、と思っています。隙を見て、@sizuhikoさんのプロジェクトであるsizuhiko/Bddなども試してみたいなぁとは長らく企んでいます!
また、実際にCIに組み込む場合には、いわゆる「ヘッドレス化」を行ってディスプレイを持たない環境でも動作するようにしてあげる(cf: Xvfb)必要や、 Selenium Webdriver自体の起動も自動化して・・・といった準備が必要になるのかと思います。
今回は触れていませんが、他にも
- 開いているページのスクリーンショットを取って書き出す
- 指定した要素の内容(htmlやinnertext)を取得する
- 指定した要素のスタイル(positionやdisplayのステータス等)を検査する
といったAPIも用意されており、夢が広がるなぁという感じがしています!
個人的には、初めて知った時には「何コレぇぇぇアツいいい」と社内で叫び・打ち震え・立ち上がり・のたうち回った記憶が・・・未だ新鮮に脳内に刻まれております。
今まで触れたことがなかった・知らなかったという方がいましたら、これを機に是非トライしてみてくださいね!
弊社では、まだ見ぬツールを探しだして悶え語り合う仲間(もだ友)や、世界で最も影響力のある人材を募集しています<●> <●>
カジュアルな気持ちで、オフィスまで遊びに来てくださいね!
Wantedly経由で、CTOと握手!!
おまけ1: セットアップについて
本記事ではPHPアプリケーションの実装を中心に記述をしましたが、上記のテストコードを動かすに当たっては以下の2点の準備が必要になります。
- Selenium Webdriverがインストールされ、立ち上がっている
- PHPUnit マニュアル – 第13章 PHPUnit と Seleniumの内容が参考になるかと思います。
- 環境によっては、正常に叩くためには(.bashrc等で)
export DISPLAY=:0
と環境変数を設定しておく必要があるかも知れません。Seleniumは立ち上がるもののブラウザが立ちあげられない・・という状況になります。
- php-webdriverが設置されている
- composer経由でインストール可能です。今回のアプリケーション+テスト用のcomposer.jsonはこちらに置いてある内容を利用しているので、ご参考にしてください
おまけ2: Selenium + php-webdriverのための参考リンク
Selenium関連
- Selenium WebDriverでWebアプリのテストが変わる @IT
- Selenium WebDriverの実践入門的な記事です。やや古いですが、概観を掴めると思います
- CentOS6.4 + Selenium の環境作成 - Qiita
- 本記事中でXvfbの導入について言及していないので、参考に。
php-webdriver関連
- facebook/php-webdriver
- php-webdriverのレポジトリ
- API Documentation
- 同レポジトリのAPIドキュメント。ただし、自動的にドキュメンテーションされたもので、メソッドやクラスの構造を知ることが出来る程度で、具体的な振る舞い等を知るには少し厳しいかな・・?という印象です
- Working with PHPUnit and Selenium Webdriver
- サンプルアプリケーションの紹介記事。
- 現在ではこの記事のコードはそのままでは動かないですが、初見時に大いに参考にさせていただきました。