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/
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 ClientJson 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 FROMCALLBACK 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 FROMCALLBACK 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-syncwdを拡張したモジュール
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)
スモークテストでいいじゃない
御清聴あざした!