pig's diary

何でも忘れるので万年初心者ね

WebDriverJs 〜果たされた約束〜

相変わらずSEO無視気味のうざいブログタイトルシリーズです。

WebDriverJs というのがあって、SeleniumJavaScriptドライバです。npm で簡単に入れられます。

$ npm install selenium-webdriver

https://code.google.com/p/selenium/wiki/WebDriverJs

WebDriverJsは私の中で話題のPromiseを全面的にサポートしています。

それで、SeleniumはPromiseと非常に相性がいいと思うんです。

Seleniumはブラウザ自体の操作や状態を知るのは得意ですが、スクリプトで変化したページの状態を知るのは苦手な部分があります。例えばこれは得意です:ページ遷移後、ページが表示されたタイミングに、ページタイトルを確認してテストする。でも、私たちが実際にテストしたいのは、もっとこういうものです:ページ内のボタンをクリックしたら、XHR通信が発生し、サーバの健康状態にもよるが、2秒後ぐらいにレスポンスが返り、ページ内の複数のDOMが書き換えられるので、その一連の動作をテストしたい。

XHR後でかつレンダリングが終わったタイミングというのは、確かに知りにくいです。XHR通信の到着直後にテストをしてもいけない。レンダリングが終わった直後にテストをしてもいけない。他でもなく、テストしたい場所がテストできる状態になってからテストしたいのです。これは、ブラウザAPIでは検出しにくい情報です。もっと緩やかな(人間の視点に近い)条件でステップを進行できないものかな? と私はかれこれ中学生くらいの時からずっと考えていました。嘘です。

それで、方法がありました。Promiseを用いたSeleniumテストです。PromiseといえばJavaScriptです。JavaScriptは私が唯一書ける言語です。ラッキーです。

Promiseは、非同期テストに最適です。Promiseは処理の塊をステップとして扱い、それらを勝手に順序よく実行してくれます。ステップを外に出し、複数のテストでステップを共有することもできます。ステップをネストさせれば(たぶんWebDriverJsでもできると思うんだが)、よくある一連の操作を1つにまとめて使うこともできます。テストがスケールするかどうかは、それが増えて来た時に必ず重要になるでしょう。Promiseなら、理解し易い形で処理を増やして行くことができるでしょう。

例えば、こういう変なページがあったとしましょう。今回これをテストします。

タイトルは無視してください。作り直すのが面倒です。この画面をしばらく眺め、pushボタンをクリックしたくなったら(貴方はきっとなる)クリックします。すると2秒後に・・・

successと表示されます。わざと2秒もかかるようにしてみました。サーバのコードはこちらです。

// Node server
app.get('/timeout', function(req, res) {
  setTimeout(function() {
    res.end(JSON.stringify({
      success: 1
    }));
  }, 2000);
});

1. クリックする、2. success と表示される。それでは、この動作を実際にテストをしてみます。mochaしか使えませんのでmochaを使います。CoffeeScriptが書けますのでCoffeeScriptで書きます。

# CoffeeScript



assert = require("assert")
webdriver = require("selenium-webdriver")
buildDriver = ->
    d = new webdriver.Builder().usingServer("http://localhost:4444/wd/hub").withCapabilities(browserName: "firefox").build()
    d.manage().window().setSize 350, 200
    d



describe "strange page", ->

    it "should display 'success' on click", (done) ->

        # DOMセレクタたち。
        BUTTON = webdriver.By.id("b")
        BOX = webdriver.By.id("box")

        # :3000にアクセスする。
        d = buildDriver()
        d.get "http://localhost:3000/"

        # クリックして、"success" と表示されるまで最大4秒まで待つ。
        d.findElement(BUTTON).click()
        d.wait (->
            # この条件式が何度も実行される
            d.findElement(BOX).getText().then (text) -> text is "success"
        ), 4000, "OMG"

        # 一応テストしてみる。
        d.findElement(BOX).getText()
        .then((text) -> assert.equal text, "success") # 100%通るはず。

        # どこかでエラーが起きたら、ここに来る。
        .then(null, (reason) ->
            d.quit().then -> throw new Error(reason)
        )

        # 問題なければテストを終了。
        .then ->
            d.quit().then -> done()

WebDriverJs は.click()や.findElement() などの関数をコールすると、Promiseオブジェクトを返します。でもthenしていないのは、コールするだけで、Promiseがスケジュールに自動的に追加されて行くからです。つまり、d.then(function(){ return d.findElement(BOX).getText() }).then(...) とも書けますが、代わりに上記のようにも淡々とスケジュールを登録していくことができます。これは npm q には無い考え方でした。

重要なのは driver.wait(condition, ms, message) です。第1引数に、Booleanを返すFunctionを渡しましょう。すると、何度も何度もこの関数が呼ばれ、trueになるまで、ステップを待機する事ができます。つまり、任意の条件を作り出し、ページがその状態になるまでステップを止めることができます。今回の2秒かかるレスポンスのテストでは、condition関数が13回呼ばれていました。

会社の人が言っていましたが、この「何度も叩く」仕組みはテストフレームワークのJasmineの非同期テストと同じ仕組みのようです。合理的です。mochaには恐らく無く、mochaでは非同期後にユーザーが done() を実行することでテストが完了する仕組みを採用しています。それで十分だし、Promiseのresolveの考え方が好きなので私は気に入っています。

$ mocha

  -----------------------------
  ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅✈
  -----------------------------

  1 test complete (6 seconds)

$

ビィィュュュォォァァァ・・・・。着陸成功です。ブラウザを立ち上げたりするのに時間がかかりましたが、Promiseのおかげで最短に近いスピードだと思います。

あとは特に書くこともないですが、mochaのデフォルトタイムアウトは2000ミリ秒なので mocha.opts に --timeout 10000 などと書いておきましょう。