※前編に関しては以下のリンクを参照ください。
PhantomJSを使ったスマホサイトテストの自動化(前編) 内容に関しては、前回お伝えしたとおり
・認証を前提としたページのテスト方法 ・Sinon.JSを利用したAjaxのレスポンスの改変 という構成になっています。
認証を前提としたページのテスト方法 みなさんは認証が必要なサイトのテストをブラウザで行う場合、いつもどのようにテストされているでしょうか?
・アカウントとパスワードを手入力してログイン後のページをチェック ・男性ユーザーと女性ユーザーで表示が変わるため、それぞれでログインし直して表示を確認 ・ユーザーのカスタマイズによって自由に表示が変えられるページを、色々なパターンで作ったアカウントを用いて1つずつ確認 ・さらには上記のようなパターンを新しいリリースが行われるたびに一から(ry 上げればキリがないのですが、昨今の複雑化したWebサイト、特にユーザー認証を挟んで利用するようなサービスではとにかくテストが大変です。
ですが、そのようなサイトでもPhantomJSを使えば上記のようなパターンを全て自動でテストすることができます。
※前回と同じくMacOSX上での動作を前提として進めていきます。
■使用するツール・ライブラリ ・Node.js (0.10.11 最新安定バージョン)
言わずと知れたサーバサイドJavaScriptです。
今回のテスト対象である認証が実装されたサンプルをNode.jsで作成しましたので、まずこれをインストールします。
Node.js公式サイト ・Express (2.5.11)
Node.js用のフレームワークです。
Express公式サイト ・jade (0.31.2)
Node.js用のテンプレートエンジンです。
jade公式サイト Node.jsのインストール 公式サイトにあるインストールパッケージを使ってインストールしてもOKですが、brewが使える場合は
$ brew install node でもインストール可能です。
※brewからのインストールの場合、最新安定版であるv0.10.11ではなくv0.10.0でインストールされますがこちらでも問題ないです。
認証が実装されたサンプル 下記URLにアップしていますので、ダウンロードして作業ディレクトリに配置して下さい。
なお、今回用意したサンプルはCookieのみを使ったあくまで簡易的な認証システムですので、実際には使用しないで下さい。
https://github.com/feb0223/1pixelTestSample201306_02 Express, jadeのインストール ダウンロードしたサンプルのpackage.jsonにインストール設定を記述していますので、
$ cd ./server_sample $ npm update と実行すればインストールされます。
※npmはNode.js用のパッケージ管理ツールです。Node.jsのインストール時に同時にインストールされます。
さて、これで準備が整いましたので、ここから実際にテストを実行していきます。
まず認証が実装されたサンプルのサーバーを立ちあげます。
$ cd ./server_sample $ node server.js この状態でブラウザから
http://localhost:8080 にアクセスするとログイン画面が表示されます。
このサンプルを操作するPhantomJSのコードは以下の通りです。
test_auth.js
(function() { var webpage = require('webpage'); var mocha = require('./lib/mocha-phantom.js').create({reporter:'spec', timeout:1000*60*5}); require('./lib/expect.js'); mocha.setup('bdd'); // テストの定義 describe('認証テスト', function() { // pageオブジェクトを作成 var page = webpage.create(); // 画面のサイズを設定 page.viewportSize = {width: 320, height: 480}; /** * 指定したアカウントの認証Cookieデータを取得 * @param {String} account * @param {String} password * @param {Function} callback */ function getAuthCookieData(account, password, callback) { // Cookieクリア phantom.clearCookies(); // 指定したページをオープン page.open('http://localhost:8080', function(status) { if (status !== 'success') { console.log('error!'); phantom.exit(); return; } // ログインボタン押下時にも再度openイベントが走ってしまうため、 // ここで終わらせる。 if (page.url.match('\/top$')) { return; } page.evaluate(function(account, password) { $('#account').val(account); $('#password').val(password); }, account, password); // ログインボタンの位置を取得 var btnClickPosition = page.evaluate(function() { var btnShowModal = $('#login_btn').get(0); var rect = btnShowModal.getBoundingClientRect(); var sx = (btnShowModal.screen) ? 0 : document.body.scrollLeft; var sy = (btnShowModal.screen) ? 0 : document.body.scrollTop; var position = { left: Math.floor(rect.left + sx), top: Math.floor(rect.top + sy), width: Math.floor(rect.width), height: Math.floor(rect.height) }; return { left: Math.round(position.left + position.width / 2), top: Math.round(position.top + position.height / 2) }; }); // ボタンをクリックしてログインする page.sendEvent('click', btnClickPosition.left, btnClickPosition.top); // submit処理のため1秒待つ setTimeout(function() { callback(phantom.cookies); }, 1000); }); } var authCookieData = {}; // テストの前処理の記述 before(function(done) { // user1,user2の認証Cookieデータを取得 getAuthCookieData('user1', 'user1password', function(user1Cookie) { authCookieData['user1'] = user1Cookie; getAuthCookieData('user2', 'user2password', function(user2Cookie) { authCookieData['user2'] = user2Cookie; done(); }); }); }); describe('user1でログイン', function() { before(function(done) { // Cookieクリア phantom.clearCookies(); // user1の認証Cookieデータをセット var cookies = authCookieData.user1; for (var i=0; i < cookies.length; i++) { phantom.addCookie(cookies[i]); } page.open('http://localhost:8080', function(status) { if (status !== 'success') { console.log('error!'); phantom.exit(); return; } // 画面のキャプチャ page.render('./capture/user1_auth.png'); done(); }); }); describe('表示チェック', function() { it('ユーザー名', function() { var welcomeText = page.evaluate(function() { return $('#main h4').text(); }); expect(welcomeText).to.be('ようこそuser1さん!'); }); }); }); describe('user2でログイン', function(done) { before(function(done) { // Cookieクリア phantom.clearCookies(); // user2の認証Cookieデータをセット var cookies = authCookieData.user2; for (var i=0; i < cookies.length; i++) { phantom.addCookie(cookies[i]); } page.open('http://localhost:8080', function(status) { if (status !== 'success') { console.log('error!'); phantom.exit(); return; } // 画面のキャプチャ page.render('./capture/user2_auth.png'); done(); }); }); describe('表示チェック', function() { it('ユーザー名', function() { var welcomeText = page.evaluate(function() { return $('#main h4').text(); }); expect(welcomeText).to.be('ようこそuser2さん!'); }); }); }); after(function() { // PhantomJSの終了 phantom.exit(); }); }); // テストの実行 var runner = mocha.run(); })(); 実行の際は、先ほどNode.jsのサーバーを立ち上げたものとは別のコンソールを立ちあげ、
$ cd ./phantomjs $ phantomjs test_auth.js という風に実行すればOKです。
一応PhantomJSでのテストの流れを解説すると、
1.PhantomJS上でログインページからアカウントとパスワードを指定してログイン。 2.ログイン後にCookie情報を抜き出して保持。 3.1~2の流れを別ユーザーでも実行してユーザーごとにCookie情報を保持。 4.ログインしたいユーザーのCookieをpage.open前にPhantomJSにセットしてログイン状態を再現。 5.ページを開くと、Cookieに認証情報が入っているため認証後のページにアクセスできる。 以上のような流れになっています。
テストの結果はどうでしたでしょうか?
正しく実行できていればコンソールに以下のように表示されると共に、/phantomjs/captureディレクトリにキャプチャ画像が保存されていると思います。
認証テスト user1でログイン 表示チェック ✓ ユーザー名 user2でログイン 表示チェック ✓ ユーザー名 2 tests complete (3 seconds) Sinon.JSを利用したAjaxのレスポンスの改変 Ajaxを使ってフロントでデータを取得してJavaScriptでDomを描画。
Webサイトの表示を速くする効果もあるため、昨今のサイトでは必ずといっていいほど使われている技術です。
ですがフロントで完結する作業だけあり、
・データ数が多い場合は「もっとみる」ボタンを表示 ・逆にデータ数が少ない場合はページのレイアウトを調整 ・Ajaxでエラーが返った場合は専用のエラーメッセージを表示する などなど・・・
先ほどの認証を前提としたページに負けないぐらいの細かい表示分けがあることと思います。
こんなケースでもPhantomJS、そしてSinon.JSを使えばAjaxのリクエストを好きに改変して色々なパターンでテストすることができます。
使用するツール・ライブラリ ・Node.js (0.10.11 最新安定バージョン)
・Sinon.JS (2.5.11)
Sinon.JS JasmineやMochaなどのテストフレームワークと組み合わせて使う、テストに関する様々なユーティリティを含んだライブラリです。
残念ながら私はそこまで詳しくはないのですが、特に便利なものではフロントのJSにおけるxhr処理の置き換えやtimer系関数の置き換えなど、普段はテストが難しい箇所でもSinon.JSを使えば細かいテストが可能になります。
更に詳しい解説については
@hokacchaさん のスライドを見てもらえれば分かりやすいと思います。
http://hokaccha.github.io/slides/sinonjs/ テスト対象のサンプル 先ほどの "認証を前提としたページのテスト" で使用したサンプルをそのまま使います。
先ほどと同じように
$ cd ./server_sample $ node server.js でサーバーを立ちあげておいてください。
テスト対象のページは
http://localhost:8080/modal です。
(前回のモーダルウィンドウの使い回しですすいませんすいません)
このサンプルを操作するPhantomJSのコードは以下の通りです。
test_sinonjs.js
(function() { var webpage = require('webpage'); var mocha = require('./lib/mocha-phantom.js').create({reporter:'spec', timeout:1000*60*5}); require('./lib/expect.js'); mocha.setup('bdd'); // テストの定義 describe('モーダルウィンドウテスト', function() { // pageオブジェクトを作成 var page = webpage.create(); // 画面のサイズを設定 page.viewportSize = {width: 320, height: 480}; // テストの前処理の記述 before(function(done) { // 指定したページをオープン page.open('http://localhost:8080/modal', function(status) { if (status !== 'success') { console.log('error!'); phantom.exit(); return; } // モーダル表示ボタンの位置を取得 var btnClickPosition = page.evaluate(function() { var btnShowModal = $('#btn_show_modal').get(0); var rect = btnShowModal.getBoundingClientRect(); var sx = (btnShowModal.screen) ? 0 : document.body.scrollLeft; var sy = (btnShowModal.screen) ? 0 : document.body.scrollTop; var position = { left: Math.floor(rect.left + sx), top: Math.floor(rect.top + sy), width: Math.floor(rect.width), height: Math.floor(rect.height) }; return { left: Math.round(position.left + position.width / 2), top: Math.round(position.top + position.height / 2) }; }); // Sinon.jsの読み込み page.injectJs('./include/sinon-1.7.1.js'); // xhrを置き換えてダミーサーバーを作成 page.evaluate(function() { var server = sinon.fakeServer.create(); var response = [200, {}, '{"title":"モーダルウィンドウテストSinon.JS", "description":"PhantomJS上でSinon.JSを使ってAjaxのレスポンスを改変しています。"}']; server.respondWith('GET', '/json/detail.json', response); server.autoRespond = true; // レスポンスを返すまでの時間をmsで指定 server.autoRespondAfter = 100; window.sinonServer = server; return; }); // ボタンをクリックしてモーダルウィンドウを表示 page.sendEvent('click', btnClickPosition.left, btnClickPosition.top); // Ajaxリクエストが発生するため1秒待つ setTimeout(function() { // ページのキャプチャ page.render('./capture/modal_capture.png'); // 非同期処理の終了 done(); }, 1000); }); }); describe('表示チェック', function() { it('ウィンドウ', function() { var modalLength = page.evaluate(function() { return $('#main .modal').length; }); expect(modalLength).to.be(1); }); it('タイトル', function() { var titleText = page.evaluate(function() { return $('#main .modal .title').text(); }); expect(titleText).to.be('モーダルウィンドウテストSinon.JS'); }); it('説明', function() { var descriptionText = page.evaluate(function() { return $('#main .modal .description').text(); }); expect(descriptionText).to.be('PhantomJS上でSinon.JSを使ってAjaxのレスポンスを改変しています。'); }); }); after(function() { // xhrをもとに戻す page.evaluate(function() { window.sinonServer.restore(); }); // PhantomJSの終了 phantom.exit(); }); }); // テストの実行 var runner = mocha.run(); })(); 実行の際も先ほどと同じようにNode.jsのサーバーを立ち上げたものとは別のコンソールを立ちあげ、
$ cd ./phantomjs $ phantomjs test_sinonjs.js という風に実行すればOKです。
テストの結果はどうでしたでしょうか?
正しく実行できていればコンソールに以下のように表示されると共に、captureディレクトリに画像が保存されていると思います。
モーダルウィンドウテスト 表示チェック ✓ ウィンドウ ✓ タイトル ✓ 説明 3 tests complete (2 seconds) キャプチャした画像
このテストサンプルでは、ページ表示後のボタンがクリックされる前のタイミングでxhrの置き換えをしてAjaxを改変しましたが、ページの表示と同時にAjaxを実行しているようなケースではこれではうまくいきません。
ですがそんなケースでも、page.onInitializedを使ってフロントのJSが読み込まれる前のタイミングでSinonJSの処理を実行してしまえばOKです。
// page.onInitializedを使った例 page.onInitialized = function() { // Sinon.jsの読み込み page.injectJs('./include/sinon-1.7.1.js'); // xhrを置き換えてダミーサーバーを作成 page.evaluate(function() { var server = sinon.fakeServer.create(); var response = [200, {}, '{"title":"モーダルウィンドウテストSinon.JS", "description":"PhantomJS上でSinon.JSを使ってAjaxのレスポンスを改変しています。"}']; server.respondWith('GET', '/json/detail.json', response); server.autoRespond = true; // レスポンスを返すまでの時間をmsで指定 server.autoRespondAfter = 100; window.sinonServer = server; return; }); }; 今回のソースを下記にアップしていますので、よければそちらもご覧ください。
https://github.com/feb0223/1pixelTestSample201306_02 終わりに 前編・後編と分けて解説したPhantomJSでのテストはどうでしたでしょうか?
テストフレームワークやNodeをある程度理解している前提で解説した部分も多かったため、分かりにくいところもあったかと思いますが、これを機にPhantomJSについて興味を持っていただければ幸いです。
そして!
そんな技術を使ったスマートフォン版Amebaもどうかよろしくお願いしますm(_ _)m
https://s.amebame.com