みなさんこんにちは!
スマホ版Ameba担当の川口です。

ちょうど一年前、同じようにJavaScriptを使ったテスト手法について記事を書かせていただいたのですが、今回も懲りずにまた同じようなテーマで再登場いたしました。

※前回の記事に関しては以下のリンクを参照ください。
JavaScriptのテスト手法

$1 pixel|サイバーエージェント公式クリエイターズブログ-PhantomJSを用いた自動キャプチャシステム

さて、スマホ版Amebaの全面リニューアルから早くも1年経ったのですが、今回はそんなスマホ版Amebaで日々自動テストツールとして活躍してもらっているPhantomJSを紹介させていただきます。

長い記事になるため、今回は前編・後編に分けて以下のような構成でお送りいたします。

●前編
・PhantomJSの概要と特徴、利点と欠点
・実際のサイトを想定したPhantomJSでのテスト方法

●後編(※多少変更になる可能性もあります。)
・認証を前提としたページのテスト方法
・Sinon.JSを利用したAjaxのレスポンスの改変

PhantomJSの概要と特徴、利点と欠点

PhantomJS公式ページ

■PhantomJSの概要
みなさんはPhantomJSと聞いたときどのようなものを想像したでしょうか?
JavaScriptの新しいライブラリ? CoffeeScriptのようにJSに変換される言語?
いえ、違います。
PhantomJSはJSのライブラリでもJSに変換される言語でも無く、コンソール上からwebkitブラウザを操作する仕組みです。
このように言われてもピンと来ないかもしれませんが、簡単に説明するとChromeやSafariなどと同種のブラウザの一つです。(Chromeは近々Blinkに変わるので今後挙動が違ってくるかもしれませんが・・・)
ただし、ChromeやSafariと違いブラウザを操作するための画面(GUI)が無いため、操作は全てJavaScriptで記述したスクリプトで行なっていくことになります。

■PhantomJSの特徴
PantomJSの特徴としてはまず、先ほど説明したように画面(GUI)が存在しないという特徴があります。
画面が存在しないのに、ページの表示をどう確認するのか?
もうお気づきかもしれませんが、PhantomJSでは任意の指定したページを開き、そして好きなタイミングでキャプチャを撮ることができます。
これだけでも日々のテストとしては十分役に立つのですが、PhantomJSでは更に

・ページ内での任意のJavaScriptコードの実行
・指定したボタンのイベントを動作(発火)させる
・Cookieの書き換え
・UserAgentの書き換え
・JavaScriptエラーの取得
・Ajaxや画像など様々なリクエストの監視


などなど、現在のWEBページ制作では欠かせない要素のテストが全てコンソール上でできてしまいます。

■PhantomJSの利点
PhantomJSの利点としてはやはり、実行環境を選ばずにかつスクリプトで動作を制御できる、という点が大きいです。
これによってWindowsやMacのみならず、CentOSやUbuntuなどのLinux系のOSでも動作させることができます。(※Linux系OSの場合日本語フォントのインストールが別途必要になります)
実際、PhantomJSを使って構築したスマホ版Amebaのキャプチャシステムも現在CentOS上で稼働しています。(記事上部の画像がキャプチャシステムです)

■PhantomJSの欠点
これまでPhantomJSの様々な特徴や利点を解説して来ましたが、やはりPhantomJSにも欠点があります。
不満に思う点はたくさんあるのですが(笑)、基本的な部分でいうと当たり前ですがwebkit系以外のブラウザでの挙動をエミュレートできないというのが最も大きいです。
スマホサイトのテストであればPhantomJSでOKですが、PCを対象としたサイトとなるとあの憎っくきIE・・・ではなく天下のIE様にも対応しなければいけませんので、ブラウザを直接操作してテストするSeleniumなどのツールを使うことになると思います。

その他実際に使っていて気になった部分だと、

・JSでDomを追加してイベントを設定した要素に対し、イベントの発火ができない
  →スマホ版AmebaのようにJSでDom構築をやっているサイトでは絶望的ですorz
・イベント発火時、対象となる要素を絶対位置で指定しなければならない
・開いたページでリダイレクトが頻繁に発生すると高い確率でPhantomJSが落ちる
・一つのページオブジェクトでページ遷移を何度も行うと高い確率でPhantomJSが落ちる
・Ajaxでデータを取得して描画するページの場合、wait処理を挟んで描画が終わるのを
 待たなければならない

  →スマホ版Amebaの画面キャプチャの際にはAjaxのリクエストを監視して全ての
   リクエストが終わったタイミングでキャプチャしています。

などがありました。

実際のサイトを想定したPhantomJSでのテスト方法(準備編)

今回のテストはMacOSX上の環境で動かす事を前提に解説しています。
申し訳ないのですが、WindowsやLinux系OSでの動作に関しては適宜そのOSに沿った形に置き換えてお読み下さい。

まず準備段階として、PhantomJSやテストのためのライブラリを用意していきます。

■使用するツール、ライブラリ
・PhantomJS (1.9)
・Mocha (1.10.0)
  テスト用のライブラリです。
  前回のJasmineと同じくビヘイビア駆動開発の概念に基づいて設計されたライブラリで、さらに詳しい解説は前回の記事を参照してください。
・expect.js (0.2.0)
  アサーション関数(テスト結果をチェックするための関数)が多数用意されたライブラリです。
  今回のテストはMochaとexpect.jsの組み合わせで記述していきます。

■PhantomJSのインストール
公式ページからダウンロードして所定の場所に配置してもらうか、Homebrewがインストールされていれば、
$ brew update && brew install phantomjs

このコマンドでインストールできます。

■Mochaのインストール
残念ながら現在Mochaの最新版はPhantomJS上での動作に対応していないため、最低限の動作に対応するように私が自前で修正したものを使っていきます。
下記ページよりダウンロードして作業ディレクトリに配置して下さい。
https://github.com/feb0223/mocha-phantom

■expect.jsのインストール
こちらは特に修正の必要なくPhantomJS上で動作します。
下記ページよりダウンロードして作業ディレクトリに配置して下さい。
https://github.com/LearnBoost/expect.js

さて、これで実行環境が整いましたのでここから実際のテストを始めていきます。

実際のサイトを想定したPhantomJSでのテスト方法(実行編)

■基本的なPhantomJSの操作
テストを実行していく前にPhantomJSの基本的な操作を解説していきます。

●require('webpage').create()
URL指定でページを表示するためのpageオブジェクトを作成します。

●page.open()
WEBページのURLを指定してページを表示します。

●page.evaluate()
page.openでページを表示した後にそのページ内で任意のJavaScriptを実行します。
今回は表示したページ内でjQueryを読み込んでいるので、evaluate内でもjQueryを使ってdomアクセスを行なっています。

●page.sendEvent()
domの位置を直接指定してClickイベントなどを発火させることができます。
イベントの発火はこのsendEventでのみ可能となっており、page.evaluateメソッドを使ってページ内でdispatchEventなどを実行してもイベントを発火させることはできません。

●page.render()
パスを指定してページのキャプチャを撮ります。
pdf,jpeg,pngに対応しており、パスの拡張子指定によって出力形式が変わります。

■テストの実行
今回のテストでは、前回と同じく(決して使い回しではないです!)

1.ボタンをクリック
2.モーダルウィンドウが表示され、そこに商品などに関する詳細情報が表示される

という流れをPhantomJSで操作しながらテストしていきます。

test.js
// node.jsと違いphantomjsでは実行ファイルを即時関数で囲まないと
// 全てグローバルスコープで定義されてしまうため、即時関数で囲む。
(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('../sample/index.html', function(status) {
if (status !== 'success') {
console.log('error!');
phantom.exit();
return;
}
// モーダル表示ボタンの位置を取得
// page.evaluateメソッドを使ってページ内部でのJavaScript実行結果を取得できる
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)
};
});

// ボタンをクリックしてモーダルウィンドウを表示
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('モーダルウィンドウテスト');
});

it('説明', function() {
var descriptionText = page.evaluate(function() {
return $('#main .modal .description').text();
});
expect(descriptionText).to.be('1pixel用のテストです。');
});
});

after(function() {
// PhantomJSの終了
phantom.exit();
});
});

// テストの実行
var runner = mocha.run();
})();

実行の際は
$ phantomjs test.js

という風にphantomjsコマンドに実行するファイルを指定して実行します。
テストが成功していればコンソールに以下のよう表示されるはずです。
モーダルウィンドウテスト
表示チェック
ウィンドウ
タイトル
説明
3 tests complete (185 ms)

PhantomJSで実際にキャプチャした画像です。
$1 pixel|サイバーエージェント公式クリエイターズブログ

今回のテストソースを下記ページにアップしていますので、よければそちらもご覧ください。
https://github.com/feb0223/1pixelTestSample201306_01

終わりに

さて、PhantomJSを使ったテストはどうでしたでしょうか?
前回の記事ではあくまでフロント側でクリック動作などを再現するものでしたが、今回は実際のブラウザとほぼ同じ環境でのテストだったためより精度の高いテストができたと思います。
次回の後編ではさらに踏み込んだテストを行なっていきますので、期待してお待ちください。