JAVASCRIPT ♥ WEB DRIVER
@kuronekomichael
福岡Haxe勉強会 feat. HTML5+α @福岡 - 第0x00回
introduction
自動テストは誰もがやりたいと思うが、敷居が高い
特にUIテストは技術的にも運用的にも難しい
少しでも作業減らしたいよね
Web Driverは意外と簡単に使えるよ
JavaScriptで書けるのでディベロッパーも頑張れるよ
メンテナブルなテストコードを書こうよ
でもさ
Web Driverとは
Googleが開発したWebアプリテストツール。
2011年にSeleniumと統合された。
Selenium2 === WebDriver
RESTfulなHTTPプロトコル「Json Wire Protocol」で
ブラウザの遠隔操作を実現
今日はRemote Web Driverの話だけやります
JsonWireProtocol: https://code.google.com/p/selenium/wiki/JsonWireProtocol
WebアプリのUIテスト
プラットフォーム毎/ブラウザ毎に、同じことを何度も何度も・・・
drawn by Cacoo: http://cacoo.com/
あ、IE9でも確認しておかないと…
drawn by Cacoo: http://cacoo.com/
Androidが追加!2.xと4.xは別物なの?!
drawn by Cacoo: http://cacoo.com/
GalaxyS4だけおかしい?AQUOS Phoneも?
何?部長がiOS7βにしただとおお?!?
drawn by Cacoo: http://cacoo.com/
GAME OVER
部長マジくたばれ
DEMO
Web Driverの仕組み
・テストコードを元に操作を要求する「WebDriver クライアント」
・HTTP経由で要求を受け取ってブラウザを操作する「WebDriver サーバ」
 ブラウザを操作するための「ドライバ」(ブラウザ毎に用意されている)
drawn by Cacoo: http://cacoo.com/ねこび∼ん by カネウチカズコ: http://ja.netbeans.org/nekobean
Web Driver Server
自前でWeb Driver Serverを準備したくないなら、Sause Labを使う手もあります https://saucelabs.com/php/se2/2
drawn by Cacoo: http://cacoo.com/ねこび∼ん by カネウチカズコ: http://ja.netbeans.org/nekobean
Web Driver Client
Json Wire Protocolに従ったhttpリクエスト/レスポンスが
処理できれば、実装言語は何でもいい
すでに言語毎に様々な実装有り(サードパーティ含む)
ねこび∼ん by カネウチカズコ: http://ja.netbeans.org/nekobean
JavaScriptでのテストコード実装
Nodeで実行
ライブラリは選択肢多数
WebDriverJs(公式)
jwebdriver
webdriver.js
burnout
wd and etc.
今回は wd を使用
wdでの実装例
var wd = require('wd'),
assert = require('assert'),
browser = wd.remote({hostname: '10.0.2.19', port: 8080});
browser.init({browserName:'android'}, function(err, sessionId) {
// ページを開く
browser.get("http://demo.basercms.net/", function(err) {
// 要素を取得
browser.elementByCssSelector('#global_menu .menu04 a', function(err, el) {
// 要素の文字列をチェック
el.text(function(err, text) {
assert.equal(text, '新着情報');
// 終了
browser.quit();
});
});
});
});
wdはサンプルも豊富なので参考に。Json Wire ProtocolとAPIの対比表は、読み方に慣れが必要かも…。
wd document: https://github.com/admc/wd
より実践的に
溢れだす欲求
ページを開く前にセッション情報(Cookie)を入れたい
ページ毎にtitleが正しいかテストしたい
要素が存在するか判定したい
アンカーをクリックしたい
要素をタップしたい
エビデンス(スクリーンショット)を取りたい
非同期に表示される要素が出てから次に進みたい
などなどなどなど
実践例
1)事前にセッション情報(Cookie)を入れる
2)ページを開いて、意図したタイトルかテスト
3)必須要素が存在するかテスト
4)特定の要素をクリックして
意図したページへ遷移するかテスト
5)ページ毎にスクリーンショットを保存
DEMO
1)事前にセッション情報を入れる
// 古いセッション情報を削除
browser.deleteAllCookies(function(err) {
// セッション情報を設定
browser.setCookie({name:'uuid', value:'...'}, function(err) {
// 続きの処理
});
});
※いったん全てのCookieを削除しているのは、Android DriverでBrowserのCookieを引き継いでしまうのを防ぐため
残念!
他にもCookieを入れる必要がありました
1)事前にセッション情報を入れる×3
// 古いセッション情報を削除
browser.deleteAllCookies(function(err) {
// セッション情報を設定
browser.setCookie({name:'uuid', value:'...'}, function(err) {
browser.setCookie({name:'cookie-P', value:'...'}, function(err) {
browser.setCookie({name:'tutorial_flag', value:'true'}, function(err) {
// 続きの処理
});
});
});
});
さあ!胡散臭くなってまりいました!
2) ページを開いてタイトルをテスト
// ページを開く
browser.get("http://ncat.me/", function(err) {
assert.ifError(err);
// タイトルが意図した文字列かテスト
browser.title(function(err, title) {
assert.ifError(err);
assert.ok(~title.indexOf('ネガネガ ネガにゃんこ'));
// 続きの処理
});
});
3) 必須要素が存在するかテスト
// 画面が表示されるまで待つ
browser.waitForVisibleByCssSelector('#mypageBtnPortal', 10 * 1000, function(err) {
// 必須要素が存在するかテスト
browser.elementByCssSelector('#mypageBtnPortal', function(err, element) {
assert.ifError(err);
// 続きの処理
});
});
4) 要素をクリックして遷移をテスト
// 特定の要素をクリック
element.click(function(err) {
assert.ifError(err);
// 遷移先ページが表示されるまで待つ
browser.waitForVisibleByCssSelector('#btnBack', 10 * 1000, function(err) {
assert.ifError(err);
// 続きの処理
});
});
5) スクリーンショットを保存
// スクリーンショットを撮る
browser.takeScreenshot(function(err, screenshot) {
assert.ifError(err);
fs.writeFile('screenshot.png', screenshot, 'base64', function(err) {
assert.ifError(err);
// もし続きがあればここに
});
});
browser.init({browserName: 'android'}, function(err, sessionId) {
assert.ifError(err);
// ページを開く
browser.get("http://ncat.me/dl/", function(err) {
assert.ifError(err);
// 古いセッション情報を削除
browser.deleteAllCookies(function(err) {
assert.ifError(err);
// セッション情報を設定
browser.setCookie({name:'uuid', value:'...'}, function(err) {
assert.ifError(err);
browser.setCookie({name:'cookie-P', value:'...'}, function(err) {
assert.ifError(err);
browser.setCookie({name:'tutorial_flag', value:'true'}, function(err) {
assert.ifError(err);
// Cookie付きで再びページを開く
browser.get("http://ncat.me/", function(err) {
assert.ifError(err);
// タイトルが意図した文字列かテスト
browser.title(function(err, title) {
assert.ifError(err);
assert.ok(~title.indexOf('ネガネガ ネガにゃんこ'));
// 画面が表示されるまで待つ
browser.waitForVisibleByCssSelector('#mypageBtnPortal', 10 * 1000, function(err) {
// 必須要素が存在するかテスト
browser.elementByCssSelector('#mypageBtnPortal', function(err, element) {
assert.ifError(err);
// 特定の要素をクリックして意図したページに遷移するかテスト
element.click(function(err) {
assert.ifError(err);
browser.waitForVisibleByCssSelector('#btnBack', 10 * 1000, function(err) {
assert.ifError(err);
// スクリーンショットを撮る
browser.takeScreenshot(function(err, screenshot) {
assert.ifError(err);
fs.writeFile('screenshot.png', screenshot, 'base64', function(err) {
assert.ifError(err);
browser.quit();
});
});
});
});
});
});
});
});
});
});
});
});
});
});
WELCOME TO
CALLBACK HELL
here come a
Callback Monster
Copyright © 2013 Warner Bros. Pictures / Picture from http://www.zekefilm.org/2013/07/11/tag-team-review-pacific-rim/
コールバック地獄
コールバックの連鎖に陥る危険については、
公式のドキュメントでも言及されている
https://code.google.com/p/selenium/wiki/WebDriverJs#Understanding_the_API
対策
Control Flow?
Promise?
・そもそも関数がまたがるのは直感的じゃない
・時系列に書きたい・読みたい
REDEMPTION FROM
CALLBACK HELL
人類には
yieldがある・・・!
Copyright © 2013 Warner Bros. Pictures / Picture from http://www.prairiedogmag.com/review-pacific-rim-delivers-quality-entertainment-but-little-else/
We don't give up
REDEMPTION FROM
CALLBACK HELL
人類には
yieldがある・・・!
Copyright © 2013 Warner Bros. Pictures / Picture from http://www.prairiedogmag.com/review-pacific-rim-delivers-quality-entertainment-but-little-else/
yield/generator
関数の実行を途中で中断して、
必要に応じて再開する機能
なんだ、夢でも見ているのか・・?
ECMA Script6で導入が決定している
1. 2006/10 FireFox2で独自実装
(ECMA Script3拡張、JavaScipt1.7)
2. ECMAScript6(harmony)に導入決定
次世代JavaScriptに入ることが確定
3. 先行してV8に実装完了
4. Chrome Canary(Chrome開発版)には既に導入済み
Node 0.12以降に導入済み
ECMAScript 6draft: http://wiki.ecmascript.org/doku.php?id=harmony:generators
yieldの簡単な使用例
function* asyncCode() {
console.log('初めの処理');
yield 1;
console.log('何か終わった後の処理');
return 2;
}
// generatorの生成(まだ関数は実行されない)
var gen = asyncCode();
// 1回めの実行
var ret = gen.next();
// コンソールには’初めの処理’が出力される
// ret === {value:1, done:false}
// 2回めの実行
ret = gen.next();
// コンソールには’何か終わった後の処理’が出力される
// ret === {value:2, done:true}
wd-sync
wdを拡張したモジュール
yieldを使ってAPIを全て同期に置換えている
実はECMAScriptのyieldは使ってない(*ノω・*)テヘ
wd-syncは fibers を使って同期を実現している
fibersはJavaScriptだけではなくCのコードで同じ機能を実現させている
他にも無理矢理実現させているモジュールもあるみたい
(関数を文字列化してsetTimeoutで無理矢理分割とか...)
wd-syncを使った実装
sync(function() {
browser.init({browserName: 'android'});
// Cookieを設定するためにいったんサイトを開く
browser.get("http://ncat.me/dl/");
// セッション情報を再設定
browser.deleteAllCookies();
browser.setCookie({name:'uuid', value:'...'});
browser.setCookie({name:'cookie-P', value:'...'});
browser.setCookie({name:'tutorial_flag', value:'true'});
// Cookieを設定したので、改めて開く
browser.get("http://ncat.me/");
// タイトルが意図した文字列かテスト
var title = browser.title();
assert.ok(~title.indexOf('ネガネガ ネガにゃんこ'));
// 画面が表示されるまで待つ
browser.waitForVisibleByCssSelector('#mypageBtnPortal', 10 * 1000);
// 必須要素が存在するかテスト
var element = browser.elementByCssSelector('#mypageBtnPortal');
assert.ok(element);
// 特定の要素をクリックして意図したページに遷移するかテスト
element.click();
browser.waitForVisibleByCssSelector('#btnBack', 10 * 1000);
// スクリーンショットを取得
var screenshot = browser.takeScreenshot();
assert.ok(screenshot);
fs.writeFileSync('screenshot.png', screenshot, 'base64');
});
ending
意外とWeb Driverは簡単
うまく動作しない時は、http req/resの中身を見る
Json Wire Protocolは理解しやすいので一読オススメ
コールバック地獄から抜けだそう
ブラウザのコードではまだ地獄が続くけど…
テストコードはシンプルが第一
やっぱりUIのテストは難しい
完璧は求めずに、やれることからやろう(not TDD)
スモークテストでいいじゃない
御清聴あざした!

JavaScriptでWebDriverのテストコードを書きましょ