いろいろ調べたけれど、まだ決められないなー、というお話です。
Webアプリが正しく動作することを、Webブラウザーを操作して確認する E2E (End-to-End) テスト。テストの記述には様々なプログラミング言語が使えます。
Selenium WebDriver + JavaScript で E2E テストをするやり方が
An Introduction to WebDriver Using the JavaScript Bindings - Tuts+ Code Tutorial
で紹介されています。
この記事は、基本となる WebDriverJS 以外に、7つのクライアントAPIライブラリーを紹介しています。どれも github で公開されていたので、スター数を調べてみました (2014/12/07時点と2015/09/12時点)。また Intern と Protractor についても調べました。
Client API | スター数 2014/12 | スター数 2015/09 | 最新スター数 | 補足 |
---|---|---|---|---|
WebDriverJS | N/A | N/A | W3Cで標準化しているAPIで書く。JavaでSelenium動かしていた人向け。 | |
WD.js | 658 | 845 | ||
WebDriver.io | 661 | 1,132 | WD.jsより短く書ける。 | |
Testium | 203 | 263 | CoffeeScript で書く。 | |
Leadfoot | 63 | 107 | Internで使われてる。WD.jsと似たAPI。 | |
Nightwatch | 2,386 | 3,221 | CI、ブラウザーテストのクラウドサービス連携。 | |
DalekJS | 481 | 600 | Selenium Server まで入った全部入り。Webサイトが派手w。 | |
Webdriver-sync | 36 | 71 | Java APIに準拠。同期型。 | |
Intern | 未計測 | 3,006 | 全部入り。単体テスト(コードカバレッジ)、CI、ブラウザーテストのクラウドサービス連携。 | |
Protractor | 未計測 | 4,355 | AngularJS ためのE2Eテストフレームワークだが AngularJS 以外にも使える。 | |
さらに、WebdriverJS, WD.js, WebdriverIO, Nightwatch, Dalek, Intern, Protractor の7つについて、実際にコードを書いてみました。 |
お題は「Googleで"webdriver"を検索した時のヒット数を標準出力に表示する」です。assert は使いません。
共通の事前準備
Mac を使います。Web ブラウザーは Chrome です。
Homebrew と Cask で node.js と java をインストールします。
$ 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 をインストールします。
$ npm install -g webdriver-manager
$ webdriver-manager update
コードを実行する前に、Selenium Server を起動しておきます。ただし、Dalek の場合は不要です。
$ webdriver-manager start
WebDriverJS
APIはこちら。豊富です。
http://selenium.googlecode.com/git/docs/api/javascript/index.html
インストール
$ npm install selenium-webdriver
コード
webdriverjs.js:
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();
});
実行
$ node webdriverjs.js
約 615,000 件 (0.11 秒)
WD.js
sendKeys() など WebDriverJS の名残があります。また、waitForElementByCss()など、APIが若干長めです。
インストール
$ npm install --save-dev wd
コード
wd.js:
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();
実行
$ node wd.js
約 615,000 件 (0.12 秒)
Webdriver.io
今回試した中では、API 名が短くて好きです。
インストール
$ npm install --save-dev webdriverio
コード
webdriverio.js:
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();
実行
$ node webdriverio.js
約 615,000 件 (0.12 秒)
Nightwatch
runner を使って実行するので、出力が綺麗です。
デフォルトのブラウザーが Firefox で、Chrome を使うためには設定ファイルが必要です。
インストール
$ npm install --save-dev nightwatch
コード
nightwatch.json:
{
"test_settings" : {
"default" : {
"silent": true,
"desiredCapabilities": {
"browserName": "chrome"
}
}
}
}
nightwatch.js:
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();
}
};
実行
$ ./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 を動かす必要はありません。
インストール
$npm install --save-dev dalek-cli dalekjs dalek-browser-chrome
コード
waitForElementが動作しないので、waitForを使いました(github)。
標準出力への表示はexecute()とlog.message()を組み合わせて使います。
dalek.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 で動かします。
$ ./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、ブラウザーテストのクラウドサービス Sauce Labs, BrowserStack との連携など。
参考: Can you add Nightwatch.js to the compare section?
Nightwatch との比較
インストール
$ 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
コード
$ mkdir tests/functional
tests/functional/google.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);
});
}
});
});
デフォルトでは BrowserStack を使う設定です。ローカルの webdriver を使うため NullTunnel を設定します。
tests/intern.js
define({
...
environments: [
{ browserName: 'chrome', platform: 'MAC' }
],
...
//tunnel: 'BrowserStackTunnel',
tunnel: 'NullTunnel',
...
//functionalSuites: [ /* 'myPackage/tests/functional' */ ],
functionalSuites: [ 'tests/functional/google' ],
...
});
実行
$ ./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
AngularJS ための E2E テストフレームワークですが、工夫をすれば AngularJS 以外の Web アプリでも使えます。assert には Jasmine が組み込まれています。
インストール
$ npm install --save-dev protractor
コード
AngularJS を待つ機能を無効化します(browser.ignoreSynchronization = true)。
AngularJS 以外の Web アプリでは element() が使えないため、シンタックスシュガー querySelector を定義して使っています。
getText() は Promise を返すので、then(callback) で結果を出力します。
protractor.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);
});
});
});
設定ファイルを作ります。
exports.config = {
seleniumAddress: 'http://localhost:4444/wd/hub',
specs: ['protractor.js']
};
実行
設定ファイルを渡して実行します。
$ 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 をやるか?
といった観点も必要でした。