WebDriver+JavaScriptでWebアプリのE2Eテスト


いろいろ調べたけれど、まだ決められないなー、というお話です。

Webアプリが正しく動作することを、Webブラウザーを操作して確認する E2E (End-to-End) テスト。テストの記述には様々なプログラミング言語が使えます。

Selenium WebDriver + JavaScript で E2E テストをするやり方が

<a href="http://code.tutsplus.com/tutorials/an-introduction-to-webdriver-using-the-javascript-bindings--cms-21855" target="_blank">An Introduction to WebDriver Using the JavaScript Bindings - Tuts+ Code Tutorial</a>

で紹介されています。

この記事は、基本となる <a href="https://code.google.com/p/selenium/wiki/WebDriverJs" target="_blank">WebDriverJS</a> 以外に、7つのクライアントAPIライブラリーを紹介しています。どれも github で公開されていたので、スター数を調べてみました (2014/12/07時点と2015/09/12時点)。また <a target="_blank" href="https://github.com/theintern/intern">Intern</a> と <a target="_blank" href="https://github.com/angular/protractor">Protractor</a> についても調べました。

| Client API | スター数 2014/12 | スター数 2015/09 | 最新スター数 | 補足 |
|---|---|---|---|---|
| <a href="https://github.com/SeleniumHQ/selenium/wiki/WebDriverJs">WebDriverJS</a> | N/A | N/A | [![GitHub stars](https://img.shields.io/github/stars/SeleniumHQ/selenium.svg?label=★)]() | <a href="//dvcs.w3.org/hg/webdriver/raw-file/default/webdriver-spec.html" target="_blank">W3Cで標準化しているAPI</a>で書く。JavaでSelenium動かしていた人向け。| 
| <a href="https://github.com/admc/wd" target="_blank">WD.js</a> | 658 | 845 | [![GitHub stars](https://img.shields.io/github/stars/admc/wd.svg?label=★)]() | |
| <a href="https://github.com/webdriverio/webdriverio/" target="_blank">WebDriver.io</a> | 661 | 1,132 | [![GitHub stars](https://img.shields.io/github/stars/webdriverio/webdriverio.svg?label=★)]() | WD.jsより短く書ける。 |
| <a href="https://github.com/groupon-testium/testium" target="_blank">Testium</a> | 203 | 263 | [![GitHub stars](https://img.shields.io/github/stars/groupon-testium/testium.svg?label=★)]() | CoffeeScript で書く。|
| <a href="https://github.com/theintern/leadfoot" target="_blank">Leadfoot</a> | 63 | 107 | [![GitHub stars](https://img.shields.io/github/stars/theintern/leadfoot.svg?label=★)]() | <a href="http://theintern.io/" target="_blank">Intern</a>で使われてる。WD.jsと似たAPI。|
| <a href="https://github.com/beatfactor/nightwatch/" target="_blank">Nightwatch</a> | 2,386 | 3,221 | [![GitHub stars](https://img.shields.io/github/stars/beatfactor/nightwatch.svg?label=★)]() | CI、ブラウザーテストのクラウドサービス連携。 |
| <a href="https://github.com/dalekjs/dalek" target="_blank">DalekJS</a> | 481 | 600 | [![GitHub stars](https://img.shields.io/github/stars/dalekjs/dalek.svg?label=★)]() | Selenium Server まで入った全部入り。Webサイトが派手w。|
| <a href="https://github.com/jsdevel/webdriver-sync" target="_blank">Webdriver-sync</a> | 36 | 71 | [![GitHub stars](https://img.shields.io/github/stars/jsdevel/webdriver-sync.svg?label=★)]() | Java APIに準拠。同期型。|
| <a target="_blank" href="https://github.com/theintern/intern">Intern</a> | 未計測 | 3,006 | [![GitHub stars](https://img.shields.io/github/stars/theintern/intern.svg?label=★)]() | 全部入り。単体テスト（コードカバレッジ）、CI、ブラウザーテストのクラウドサービス連携。 |
| <a target="_blank" href="https://github.com/angular/protractor">Protractor</a> | 未計測 | 4,355 | [![GitHub stars](https://img.shields.io/github/stars/angular/protractor.svg?label=★)]() | <a href="https://angularjs.org/">AngularJS</a> ためのE2Eテストフレームワークだが AngularJS 以外にも使える。 |
さらに、WebdriverJS, WD.js, WebdriverIO, Nightwatch, Dalek, Intern, Protractor の7つについて、実際にコードを書いてみました。

お題は「Googleで"webdriver"を検索した時のヒット数を標準出力に表示する」です。assert は使いません。

# 共通の事前準備

Mac を使います。Web ブラウザーは Chrome です。
Homebrew と Cask で node.js と java をインストールします。

```console
$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
$ brew install node
$ brew brew install brew-cask
$ brew cask install java
```

さらに webdriver-manager をインストールします。webdriver-manager update コマンドで selenium-server, chrome-driver をインストールします。

```console
$ npm install -g webdriver-manager
$ webdriver-manager update
```

コードを実行する前に、Selenium Server を起動しておきます。ただし、Dalek の場合は不要です。

```console
$ webdriver-manager start
```

# WebDriverJS

APIはこちら。豊富です。
http://selenium.googlecode.com/git/docs/api/javascript/index.html

### インストール

```console
$ npm install selenium-webdriver
```

### コード
webdriverjs.js:

```javascript
var webdriver = require('selenium-webdriver');
var By = webdriver.By;

//WebブラウザーはChrome
var driver = new webdriver.Builder().
   withCapabilities(webdriver.Capabilities.chrome()).
   build();
var $ = driver.findElement.bind(driver);

//Googleを開く。
driver.get('http://www.google.com');

//検索ボックスにwebdriverと入力する。
$(By.name('q')).sendKeys('webdriver');

//検索ボタンを押す。
$(By.name('btnG')).click();

//ヒット数が表示されるまで待つ。
var timeoutMSec = 2000;
driver.wait(webdriver.until.elementLocated(By.id('resultStats')), timeoutMSec)
.then(function() { //waitした後はthenでつなぐ
    $(By.id('resultStats')).getText().then(function(text) {
        console.log(text);
    });
})
.then(function() {
    driver.quit();
});
```

### 実行

```console
$ node webdriverjs.js 
約 615,000 件 （0.11 秒） 
```
# WD.js
sendKeys() など WebDriverJS の名残があります。また、waitForElementByCss()など、APIが若干長めです。

### インストール

```console
$ npm install --save-dev wd
```

### コード

wd.js:

```javascript
var wd = require('wd');
var browser = wd.promiseChainRemote();
var timeoutMSec = 1000;
browser
    .init({
        browserName: 'chrome'
    })
    .get('http://www.google.com')
    .elementByName('q')
    .sendKeys('webdriver')
    .elementByName('btnG')
    .click()
    .waitForElementByCss('#resultStats', timeoutMSec)
    .text(function(err, text) {
        console.log(text);
    })
    .quit();
```

### 実行

```console
$ node wd.js
約 615,000 件 （0.12 秒）
```

# Webdriver.io

今回試した中では、API 名が短くて好きです。

### インストール

```console
$ npm install --save-dev webdriverio
```

### コード
webdriverio.js:

```javascript
var timeoutMSec = 1000;
var webdriverio = require('webdriverio')
    .remote({
        desiredCapabilities: {
            browserName: 'chrome'
        }
    })
    .init()
    .url('http://www.google.com')
    .setValue('[name="q"]', 'webdriver')
    .click('[name="btnG"]')
    .waitForExist('#resultStats', timeoutMSec)
    .getText('#resultStats', function(err, text) {
        console.log(text);
    })
    .end();
```

### 実行

```console
$ node webdriverio.js 
約 615,000 件 （0.12 秒） 
```
# Nightwatch

runner を使って実行するので、出力が綺麗です。
デフォルトのブラウザーが Firefox で、Chrome を使うためには設定ファイルが必要です。

### インストール

```console
$ npm install --save-dev nightwatch
```

### コード

nightwatch.json:

```json
{
  "test_settings" : {
    "default" : {
      "silent": true,
      "desiredCapabilities": {
        "browserName": "chrome"
      }
    }
  }
}
```

nightwatch.js:

```javascript
module.exports = {
  "webdriverの検索ヒット数を表示する" : function (browser) {
    var timeoutMSec = 1000;
    browser
      .url("http://www.google.com")
      .setValue('[name="q"]', 'webdriver')
      .click('[name="btnG"]')
      .waitForElementPresent('#resultStats', timeoutMSec)
      .getText('#resultStats', function(res) {
          console.log(res.value);
      })
      .end();
  }
};
```

### 実行

```console
$ ./node_modules/.bin/nightwatch -t nightwatch.js 

[Nightwatch] Test Suite
=======================

Running:  webdriverの検索ヒット数を表示する 

✔  Element <#resultStats> was present after 1038 milliseconds.
約 615,000 件 （0.14 秒） 

OK. 1 total assertions passed. (11.812s)
```

# DalekJS

Selenium server を動かす必要はありません。

### インストール

```console
$npm install --save-dev dalek-cli dalekjs dalek-browser-chrome 
```

### コード
waitForElementが動作しないので、waitForを使いました(<a href="https://github.com/dalekjs/dalek/issues/22" target="_blank">github</a>)。
標準出力への表示はexecute()とlog.message()を組み合わせて使います。

dalek.js:

```js
module.exports = {
  'webdriverのヒット数を表示する': function (test) {
    var timeoutMSec = 10000;
    test
      .open("http://www.google.com")
      .setValue('[name="q"]', 'webdriver')
      .click('[name="btnG"]')
      //.waitForElement('#resultStats', timeoutMSec)
      .waitFor(function() {
        return Boolean(document.querySelector('#resultStats'));
      }, [], timeoutMSec)
      .execute(function() {
        var result = document.querySelector('#resultStats').innerText;
        this.data('result', result);
      })
      .log.message(function() {
        return test.data('result');
      })
      .done();
  }
};
```

### 実行

引数 -b chrome を与えて Chrome で動かします。
デフォルトではヘッドレスブラウザーの PhantomJS で動かします。

```console
$ ./node_modules/.bin/dalek dalek.js -b chrome
Running tests
Running Browser: Google Chrome
OS: Mac OS X 10.10.1 x86_64
Browser Version: 39.0.2171.71

RUNNING TEST - "webdriverのヒット数を表示する"
▶ OPEN http://www.google.com
▶ SETVALUE [name="q"]
▶ CLICK [name="btnG"]
▶ WAITFOR 
▶ EXECUTE 
☁ [USER] MESSAGE: 約 615,000 件 （0.38 秒） 
✔ 0 Assertions run
✔ TEST - "webdriverのヒット数を表示する" SUCCEEDED

 0/0 assertions passed. Elapsed Time: 4.44 sec 
```
# intern

全部入りです。単体テスト（コードカバレッジ）、CI、ブラウザーテストのクラウドサービス <a href="https://saucelabs.com/" target="_blank">Sauce Labs</a>, <a href="https://www.browserstack.com/" target="_blank">BrowserStack</a> との連携など。

参考: <a href="https://github.com/theintern/intern/issues/232" target="_blank">Can you add Nightwatch.js to the compare section?</a>



Nightwatch との比較

### インストール
```console
$ npm install --save-dev intern
$ mkdir tests
$ cp node_modules/intern/tests/example.intern.js tests/intern.js
$ ./node_modules/.bin/intern-client config=tests/intern
0/0 tests failed

```

### コード

```console
$ mkdir tests/functional
```

tests/functional/google.js:

```js
define(function (require) {
    var registerSuite = require('intern!object');
    var assert = require('intern/chai!assert');

    registerSuite({
        name: 'intern',
        'greeting form': function () {
            return this.remote
                .get('http://www.google.com')
                .setFindTimeout(5000)
                .findByCssSelector('[name="q"]')
                    .type('webdriver')
                    .end()
                .findByCssSelector('[name="btnG"]')
                    .click()
                    .end()
                .setFindTimeout(5000)
                .findByCssSelector('#resultStats')
                .getVisibleText()
                .then(function (text) {
                    console.log(text);
                });
        }
    });
});
```

デフォルトでは <a href="https://www.browserstack.com/" target="_blank">BrowserStack</a> を使う設定です。ローカルの webdriver を使うため NullTunnel を設定します。

tests/intern.js

```js
define({
...
  environments: [
    { browserName: 'chrome', platform: 'MAC' }
  ],
...
  //tunnel: 'BrowserStackTunnel',
  tunnel: 'NullTunnel',
...
  //functionalSuites: [ /* 'myPackage/tests/functional' */ ],
  functionalSuites: [ 'tests/functional/google' ],
...
});
```

### 実行
```console
$ ./node_modules/.bin/intern-runner config=tests/intern
Listening on 0.0.0.0:9000
Tunnel started
‣ Created session chrome on MAC (fd9b05d8-ad42-4b51-ac27-4ce898fc7a66)
約 9,590,000 件 （0.24 秒）  
✓ chrome on MAC - intern - greeting form (1.851s)
No unit test coverage for chrome on MAC
chrome on MAC: 0/1 tests failed


TOTAL: tested 1 platforms, 0/1 tests failed
```

# Protractor

<a href="https://angularjs.org/">AngularJS</a> ための E2E テストフレームワークですが、工夫をすれば AngularJS 以外の Web アプリでも使えます。assert には Jasmine が組み込まれています。

### インストール
```console
$ npm install --save-dev protractor
```

### コード

AngularJS を待つ機能を無効化します(browser.ignoreSynchronization = true)。

AngularJS 以外の Web アプリでは element() が使えないため、シンタックスシュガー querySelector を定義して使っています。

getText() は Promise を返すので、then(callback) で結果を出力します。

protractor.js:

```js
browser.ignoreSynchronization = true; // for non-angular web app

function querySelector(selector) { // for non-angular web app
    browser.wait(function () {
        return browser.isElementPresent(by.css(selector));
    }, 5000);
    return browser.findElement(by.css(selector));
}

describe('protractor example', function() {
    it('shows searched results count for webdriver', function() {
        browser.get('http://www.google.co.jp');
        querySelector('[name="q"]').sendKeys('webdriver');
        querySelector('[name="btnG"]').click();
        querySelector('#resultStats').getText().then(function (text) {
            console.log(text);
        });
    });
});
```

設定ファイルを作ります。

```js
exports.config = {
    seleniumAddress: 'http://localhost:4444/wd/hub',
    specs: ['protractor.js']
};
```

### 実行

設定ファイルを渡して実行します。

```console
$ protractor conf.js
Using the selenium server at http://localhost:4444/wd/hub
[launcher] Running 1 instances of WebDriver
約 323,000 件 （0.24 秒） 
.

Finished in 1.897 seconds
1 test, 0 assertions, 0 failures

[launcher] 0 instance(s) of WebDriver still running
[launcher] chrome #1 passed
```

# どれを選ぶか

調べれば調べるほど決められなくなりますね・・・。コードを builder pattern (fluent interface) で書けるものも多く、選択の決め手にはでしづらいです。

  - チームメンバーが好む assert が使えるか (assert を変更できるか) ？
  - PageObject パターンを適用してテストを構造化するか？
  - End-to-End (E2E) / Functional テストだけでなく、Unit テストも統合するか？カバレッジも計算するか？
  - SauseLabs,　BrowserStack といったクラウドサービスを使うか？
  - CI をやるか？

といった観点も必要でした。