読者です 読者をやめる 読者になる 読者になる

undefined

bokuweb.me

PhantomJS + MochaでクライアントサイドJavaScriptのテストをしよう


ブラウザゲームとかを作っているとなかなかテストが難しくどうしたらいいんだろうと調べていたらPhantomJS + Mochaという組み合わせを見つけたので勉強してみました。

PhantomJSとは?

ブラウザです。JavaScriptのAPIを通じて制御できるヘッドレス(画面のない)Webkitブラウザらしいです。 まだ試せてませんがCanvasやSVGにも対応してるっぽい。

インストール

http://phantomjs.org/download.htmlよりダウンロードしPATHを通すことで使用できます。

Macでhomebrewが入って入れば以下でOK。

$ brew update && brew install phantomjs

PhantomJS準備運動

まずは以下のようなhello.jsを用意します。

・hello.js

var page = require('webpage').create();
page.open('http://www.google.co.jp', function () {
    page.render('google.png');
    phantom.exit();
});

以下のように実行します。

> phantomjs hello.js

f:id:bokuweb:20150204195448p:plain

これでGoogleのスクリーンショットがとれます。(スクリーンショットをとったのは節分です。鬼がかわいい) windowサイズもJavaScriptで変更できるし、これだけでも色々応用できそう。

Mochaとは?

node.jsやブラウザから実行できるテストフレームワークです。

インストール

npmでいれてもいいし、ブラウザで実行するなら"mocha.css"、"mocha.js"のみ持ってきたらよいようです。 今回の例ではブラウザで実行させるため、以下より"mocha.css"、"mocha.js"をダウンロードしておきます。

https://github.com/mochajs/mocha

あと、Mochaはテスティングフレームワークのみで、アサーションは別のライブラリを用いる必要があるようです。 調べたところ、今回はよさげなchaiを使用します。

http://chaijs.com/

こちらもブラウザで使用するためhttp://chaijs.com/chai.jsよりダウンロードしておきます。

Mocha準備運動

こんな感じでテストができるよーってサンプルです。 加算するadd関数をテストしています。

・test.html

<html>
    <head>
        <meta charset="utf-8" />
        <link rel="stylesheet" href="mocha.css" />
    </head>
    <body>
        <div id="mocha"></div>
        <script src="chai.js"></script>
        <script src="mocha.js"></script>
        <script>
          var add = function (arg1, arg2) {
              return arg1 + arg2;
          }
          mocha.setup('bdd');
          expect = chai.expect;

          describe('add test', function() {
            it('should return 3 when arg1 is 1 and arg2 is 2', function() {
              expect(add(1,2)).to.be.equal(3);
            });
          });

          mocha.run();
        </script>
    </body>
</html>

test.htmlをブラウザで開くと以下のようにテスト結果が出力されます。

f:id:bokuweb:20150204195542j:plain

PhantomJS + MochaでのクライアントJavaScriptのテスト準備

ようやく本題。PhantomJS + Mochaでテストの準備をする。 で、いきなりですが、問題はMochaがPhantomJS上での動作に対応していないようです。 以下を参考にさせてもらいました。

PhantomJSを使ったスマホサイトテストの自動化(前編)|1 pixel|サイバーエージェント公式クリエイターズブログ

ここでPhantomJSで動作するよう修正したものが公開されているので使用させてもらいます。

feb0223/mocha-phantom · GitHub

テスト対象ページ

簡単に今回はこんなので試してみます。 画像を重ねておいて、クリックするとフェードインします。 バージョンはなんでもいいですが(たぶん)jqueryも使用します。

・index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <style>
      img.icon {
        position: absolute;
        top:50%;
        left:50%;
        margin:-40px 0 0 -40px;
      }
      img.icon-over {
        position: absolute;
        top:50%;
        left:50%;
        margin:-40px 0 0 -40px;
        z-index: 999;
        display:none;
      }
    </style>
</head>
<body>
    <img src="shadow.png" class="icon">
    <img src="icon.png" class="icon-over">
    <script src="jquery-1.11.2.min.js"></script>
    <script>
     jQuery(function($) {
       jQuery('img.icon').click(
          function () {
           console.log("click!!");
            jQuery('img.icon-over').fadeIn();
          }
        );
      });
   </script>
</body>
</html>

テストコード

こっからはCoffeeScriptです。 流れはこんな感じです。

  1. mocha, chaiの読み込み
  2. index.htmlをオープン、キャプチャをとり、jqueryを読み込み
  3. 画像の座標、フェードインする画像のdisplayプロパティを取得
  4. クリック前のdisplayプロパティが"none"であることをテスト
  5. 画像の座標をクリックし、1秒待つ
  6. クリック後のキャプチャを取得、クリック後のdisplayプロパティが"block"であることをテスト

これだけだけど、大分はまりました。

page = require('webpage').create()
mocha = require('./lib/mocha-phantom.js').create(reporter:'spec', timeout:1000*10)
chai = require('./lib/chai.js')
expect = chai.expect

mocha.setup('bdd')

describe 'client javascript sample', ->
  # 画面のサイズを設定
  page.viewportSize =
    width: 800
    height: 600

  before (done)->
    # 指定したページをオープン
    page.open 'index.html', ->
      # ページ読み込み後のキャプチャを取得
      page.render('before_click.png')
      # jqueryのインクルード
      page.includeJs "jquery-1.11.2.min.js", ->
        console.log "includeJS"
        done()
      page.onConsoleMessage = (msg, lineNum, sourceId)-> console.log msg
    return

  describe 'image click test', ->
    it 'fadein image when click image', ()->

      # imageの座標を取得する
      position = page.evaluate -> $('img.icon').position()

      # fadeinする画像のdisplayプロパティを取得
      displayVal = page.evaluate -> $('img.icon-over').css('display')

      # クリック前のdisplayプロパティが"none"であることをテスト
      it 'should fade in image property is none before click', -> expect(displayVal).to.be.equal("none")

      # imageの座標をクリック
      page.sendEvent('click', position.left+1, position.top+1)

      # fadeIn時間を待つ
      setTimeout ()->
        # クリック後のキャプチャを取得
        page.render('after_click.png')

        # fadeinした画像のdisplayプロパティを取得
        displayVal = page.evaluate ()-> $('img.icon-over').css('display')

        # クリック後のdisplayプロパティが"block"であることをテスト
        it 'should fade in image property is none before click', -> expect(displayVal).to.be.equal("block")
        phantom.exit()
      , 1000
      return
    return

# テストの実行
mocha.run()

実行

> phantomjs test.js

f:id:bokuweb:20150204211236p:plain

テスト後に"before_click.png"と"after_click.png"が生成されているかと思います。

・before_click.png

f:id:bokuweb:20150204195700p:plain

・after_click.png

f:id:bokuweb:20150204195722p:plain

はまった点

page.evaluateを理解してなかった

Evaluates the given function in the context of the web page. The execution is sandboxed, the web page has no access to the phantom object and it can't probe its own setting.

evaluateメソッドはWeb Pageのコンテキストで評価されます。この実行はサンドボックス化されているため、PhantomJSと直接やりとりできません。

http://murayama.hatenablog.com/entry/2013/06/24/065633より

例えば、page.evaluate内でpage.evaluate ->console.log $('img.icon').position()ってしてもWeb Pageのコンテキストで評価されてしまい、PhantomJS側には何も表示されません。これはonConsoleMessageを使うことで対処できるようです。具体的にはページオープン時にpage.onConsoleMessage = (msg, lineNum, sourceId)-> console.log msgとしておけばPhantomJS側のコンソールに吐かれます。

こちらも上記参考サイトに詳細が書かれていますが、PhantomJS側とWeb Page側とで値をやり取りするには、evaluateメソッド呼び出しの引数や戻り値を使えばいいようです。 position = page.evaluate -> $('img.icon').position()でWeb Page側の画像座標を取得したり、displayVal = page.evaluate -> $('img.icon-over').css('display')でCSSプロパティを取得したりしています。 ただし

受け渡し可能なデータはJSONにシリアライズ可能なオブジェクトに限定されます。クロージャや関数オブジェクトを使ってやりとりすることはできません。

らしいです。

やってみて

  • PhantomJS上でMochaを使うかどうかはおいといて、PhantomJSだけでも使いこなせればめちゃめちゃ便利な気がする
  • アプリケーションによってはキャプチャの差分を取って合致しているかどうかの判定処理まで入れてしまえば完全な自動化もできる?(canvasで取り込んでpixelベースの比較をしてやるとか?わかんないけど)
  • PhantomJSはwebkitなので、他のブラウザはseleniumとか使わなきゃ?
  • スマホページでこそ真価を発揮するかも
  • ただし、参考サイト(http://ameblo.jp/ca-1pixel/entry-11549761391.html)にあるように「JSでDomを追加してイベントを設定した要素に対し、イベントの発火ができない」とか不便な点もあるよう
  • なかなかおもしろかった

リポジトリ

めんどくさいので全部ごっちゃだけどあげときます。

bokuweb/mocha-phantomjs-test · GitHub

参考にしたサイト

http://ameblo.jp/ca-1pixel/entry-11549761391.html http://murayama.hatenablog.com/entry/2013/06/24/065633 http://webtech-walker.com/archive/2012/10/mocha_phantomjs_travisci.html