アシアルブログ

アシアルの中の人が技術と想いのたけをつづるブログです

タスクランナーとの親和性を向上。AppiumをNode.jsで動かす

前回までのコードはRubyで書いていましたが、MonacaアプリはHTML5/JavaScriptが主な利用言語になります。そこで今回はアプリを動かすスクリプトをNode.jsベースに書き直してみます。



これがうまく動けば、Gulpのようなテストランナーと組み合わせることで、コードの変更からテストまでがとてもスムーズになるはずです。DevOpsを積極的に進める上でも役立つのではないでしょうか。



Appiumのインストール、起動までは前回までの記事を参照してください。



Node.jsの設定



Node.jsについてはあらかじめインストールされていることとします。まだ未インストールの方はNode.jsよりダウンロード、インストールしてください。



まずテストスクリプトを書くフォルダを作成します。





mkdir appium_test
cd appium_test
npm init


そして必要なライブラリをインストールします。





npm install wd --save


これで準備は完了です。テストをスクリプトを書きます。まずAppiumに接続するまでの処理です。





"use strict";

var wd = require("wd");
var browser = wd.promiseChainRemote("0.0.0.0", 4723);


今回はローカルにAppiumサーバを立てています。そして接続する際の情報を定義します。今回はiOSシミュレータ向けに書いています。





var desired = {
	"appium-version": "1.0",
	platformName: "iOS",
	deviceName: "iPhone 6 Plus",
	app: "/Monaca_project_path/platforms/ios/build/emulator/AppiumTest.app"
};


接続がうまくいった場合は、アプリの起動した状態になっているはずです。





browser.init(desired).then(function() {
  // この中にテストの処理を書きます
});


実際の処理ですが、XPathでエレメントを取得しなければなりません。これがとてもネストが深く、分かりづらいのでGUI版Appiumのインスペクタを使うのをお勧めします。Recordボタンを押して、要素をクリックすると、そこまでのXPathが表示されます。





Appium Inspector



そしてXPathが分かったらコードに反映していきます。例えば最初の画面のHelloWorld!を知りたい場合は次のように書きます。





browser.elementByXPath("//UIAApplication[1]/UIAWindow[1]/UIAScrollView[1]/UIAWebView[1]/UIAStaticText[1]").text().then(function(text) {
  console.log(text);  // -> HelloWorld!
});


全体としてPromiseで作られていますので、text()メソッドを実行しても {status: “pending”} が返ってくるだけです。then()でつないでテキストを受け取る必要があります。



そして非同期ということもあって、テキストの取得とクリックなどの処理をそのまま書いてしまうと、text()メソッドが間に合わずに画面遷移がはじまってしまいます。そこで、一画面ずつ順番に処理を行っていきます。





// テキストを取得
browser.elementByXPath("//UIAApplication[1]/UIAWindow[1]/UIAScrollView[1]/UIAWebView[1]/UIAStaticText[1]").text().then(function(text) {
  console.log(text);
  // タップ処理
  browser.elementByXPath("//UIAApplication[1]/UIAWindow[1]/UIAScrollView[1]/UIAWebView[1]/UIALink[1]/UIAStaticText[1]").click().then(function() {
    // タップした後の処理
  });
});


最後にアプリを終了しますが、この時も非同期処理で行われているのを意識しないといけません。





browser.elementByXPath("//UIAApplication[1]/UIAWindow[1]/UIAScrollView[1]/UIAWebView[1]/UIALink[1]/UIAStaticText[1]").click().then(function() {
  // 処理を遅らせないといきなり終了してしまいます
  setTimeout(function() {
    browser.quit();
  }, 5000);
  // ここに browser.quit(); を書くといきなりアプリが終了します
  // browser.quit();
});


実際に実行した時の動画です。





テストコードを実行



最後に全体のコードです。






"use strict";

var wd = require("wd");

var desired = {
	"appium-version": "1.0",
	platformName: "iOS",
	deviceName: "iPhone 6 Plus",
	app: "/Volumes/SD/Dropbox/DevRel/Monaca/AppiumTest/platforms/ios/build/emulator/AppiumTest.app"
};

var browser = wd.promiseChainRemote("0.0.0.0", 4723);

browser.init(desired).then(function() {
	browser.elementByXPath("//UIAApplication[1]/UIAWindow[1]/UIAScrollView[1]/UIAWebView[1]/UIAStaticText[1]").text().then(function(text) {
    console.log(text);
    browser.elementByXPath("//UIAApplication[1]/UIAWindow[1]/UIAScrollView[1]/UIAWebView[1]/UIALink[1]/UIAStaticText[1]").click().then(function() {
      setTimeout(function() {
        browser.quit();
      }, 5000);
    });
  });
}).done();


注意点



JavaScriptで書く時の注意点としては、



  1. XPathで書く必要がある(ないかも知れませんがクラスでどう指定するのかが分かりませんでした…)
  2. 全体的に非同期処理


となっています。XPathは慣れればさほど難しくないかも知れませんが、Appiumのインスペクタを積極的に活用した方が良いでしょう。ただし画面の変化に対して弱くなってしまうように見えます。非同期についてはネストが深くなってしまう傾向がありますが、テストを適切に分離することで深くなりすぎるのを防げるかと思います。



JavaScriptを使う利点としてはRubyiOSをテストする際にあった落ちるエラーが出なかったので、テストの品質が高くなりそうです。また、Appium自体がNode.jsでできているので親和性が高いかと思います。






Web開発ではGulpを使ってテストを定期的に実行するのが当たり前になっています。Monacaを使ったハイブリッドアプリ開発においても同じようにテストを細かく行うことで品質の高いアプリ開発ができるようになるでしょう。

MonacaアプリのUIテストに。Appiumを試す(Android編)

前回のiOSシミュレータに続いて今回はAndroidエミュレータを使ってAppiumを試してみたいと思います。AppiumはWeb開発時のUIテストでよく知られているSeleniumiOS/Androidアプリに対応させたソフトウェアです。





Appiumのサイト



今回はこのAppiumをMonacaアプリ(Android)で動かす方法について紹介します。



CLIMonacaの準備



Appiumはローカルで動かしますのでMonaca.ioではなくCLI版のMonacaを使います。そのためnode.js/npmはインストールされているものとします。





$ npm install monaca -g


インストールしたらログインを行います。





$ monaca login



ログインしたら新しいプロジェクトを作成してみましょう。今回はHelloWorldアプリにしました。





$ monaca create Hello
Which project template do you want to use?

1: Hello World App
2: Minimum Template
  :
21: NIFTY Cloud mobile backend
22: Onsen UI Minimum Template for Universal App

Type number>1
Project created successfully.


Cordovaコマンドのインストール



次にCordovaコマンドをインストールして、適当なプロジェクトを作成します。





$ npm install cordova -g
$ cordova create HelloCordova
$ cd HelloCordova


今回はAndroidをプラットフォームとして追加します。





$ cordova platform add android


そうするとこのようなファイル構成になります。





Cordovaプロジェクトのファイル構成



まずCordovaプロジェクト上でビルドをします。





$ cordova build android


そうすると platforms/android/src 以下に com というディレクトリが作られているはずです(ない場合はMonacaプロジェクトのAndroidManifest.xmlをCordova上のものに差し替えた後、cordova build androidMonacaプロジェクト上で実行してください)。その後、platforms/android 以下のファイルを Monacaプロジェクトの platforms/android 以下に移動します。Monacaアプリでも同名のディレクトリもありますので、元々入っているものは消さずに、その中身は移動するようにしてください(res以下のXMLファイルなど)。ファイルの配置が変わってしまうとMonaca IDEでビルドできなくなる可能性があります。Appiumで使うコードは元のコードから分離し(Gitでcloneするなど)、元々のMonacaプロジェクトには影響しないようにしてください。





ファイルの移動



これで準備が完了です。CordovaプロジェクトとMonacaプロジェクトの互換性が向上していることでこのような流用が可能になっています。



ただし、Monacaプロジェクト上にこれらのファイルがあるのは良くないので、MonacaプロジェクトをGitに登録してこれをcloneし、そこにCordovaプロジェクトのファイルを入れつつ.gitignoreで元のMonacaプロジェクトには影響しないようにしておくのが良いかと思います。



Appiumのインストール



AppiumはiOS9への対応が進められているベータ版を使っています。





$ npm install appium@beta -g


Xcode7にするとインストール時に library not found for -lgcc_s.10.5 というエラーが発生する可能性があります。その時には下記コマンドを実行することで回避することができます(自己責任でお願いします)。





$ cd /usr/local/lib  & & sudo ln -s ../../lib/libSystem.B.dylib libgcc_s.10.5.dylib


Rubyライブラリのインストール



今回はRubyでテストコードを書いてみます。まずRubygemsでライブラリをインストールします。





$ gem install --no-rdoc --no-ri appium_lib


これで準備は完了です。



アプリをビルドする



テストを行うにあたって一旦ビルドを行う必要があります。Monacaアプリのディレクトリに移動して実行します。





$ monaca build android


これでアプリがビルドされればOKです。



デモコードを書いてみる



まずはテストまでいかず、簡易的に動かしてみたいと思います。





require 'appium_lib'

capabilities = {
   app: "/path_to/platforms/android/build/outputs/apk/android-debug.apk",
   platformName: "Android",
   deviceName: "Android",
   defaultCommandTimeout:7200,
   debugLogSpacing: true
}

@wd = Appium::Driver.new(caps: capabilities[:android])
Appium.promote_appium_methods Object
@wd.start_driver
sleep(5)
@wd.set_context("WEBVIEW_com.example.helloworld")
h1 = @wd.find_elements(:xpath, '//h1')[0]
puts h1.text
a = @wd.find_elements(:xpath, '//a')[0]
a.click
sleep(20)
driver_quit


appオプションは先ほどビルドしてできたアプリのパスを指定してください。driver_quitを実行するとすぐに終了してしまうので20秒のスリープを入れています。clickのようなイベントも使えますのでアプリの画面を遷移しつつ操作をDOMの状態をチェックすると言ったこともできます。



後はこのコードを実行すればOKです。以下はデモで動かしたところですが、実際にはもっと時間がかかります。







注意点



エラーについて



Androidではセッションがたびたび切れることがあります。そうした時には @wd.start_driver を再度実行してください。



iOSとの違いについて



AndroidではXPathを使ったオブジェクトの取得になります。また、コンテキストをWEBVIEW_〜ではじまるものにセットする必要があります。そのため似たような処理をテストする場合においてもiOS/Android版で共通のテストにはしづらそうです。



実機でのテストについて



Androidエミュレータなので実機を接続してもさほど変わらずテストが行えるはずです(未検証)。エミュレータIntel版にしても速度が遅くなってしまうので実機テストの方が良いかもしれません。






ハイブリッドアプリのUIテストについては情報が多くなく、まだまだ手探りのところが多いのですが検証しつつ情報を出せればと思います。良いTipsがあればぜひ共有してください!

MonacaアプリのUIテストに。Appiumを試す

アプリのテストを行う方法は幾つかあります。まず機能レベルでのテストを行うユニットテストがあります。また、サーバを使っている場合は負荷テストを行うこともあるでしょう。しかしこれらのテストだけでは分からない、実際の操作を通じて意図した通りに動いているか確認するUIテストがあります。アプリにおいてはUI/UXが大事なので特に重要なテストです。



Webの世界ではUIテストを行う仕組みとしてSeleniumがよく知られています。それをiOS/Androidアプリに対して適用したのがAppiumです。





Appiumのサイト



今回はこのAppiumをMonacaアプリで動かす方法について紹介します。



CLIMonacaの準備



Appiumはローカルで動かしますのでMonaca.ioではなくCLI版のMonacaを使います。そのためnode.js/npmはインストールされているものとします。





$ npm install monaca -g


インストールしたらログインを行います。





$ monaca login


ログインしたら新しいプロジェクトを作成してみましょう。今回はHelloWorldアプリにしました。





$ monaca create Hello
Which project template do you want to use?

1: Hello World App
2: Minimum Template
  :
21: NIFTY Cloud mobile backend
22: Onsen UI Minimum Template for Universal App

Type number>1
Project created successfully.


Cordovaコマンドのインストール



次にCordovaコマンドをインストールして、適当なプロジェクトを作成します。





$ npm install cordova -g
$ cordova create HelloCordova
$ cd HelloCordova


今回はiOSをプラットフォームとして追加します。





$ cordova platform add ios
Adding ios project...
iOS project created with cordova-ios@3.9.1
Discovered plugin "cordova-plugin-whitelist" in config.xml. Installing to the project
Fetching plugin "cordova-plugin-whitelist@1" via npm
Installing "cordova-plugin-whitelist" for ios


そうするとこのようなファイル構成になります。





Cordovaプロジェクトのファイル構成



platforms/ios 以下のファイルを Monacaプロジェクトの platforms/ios 以下に移動します。





ファイルの移動



これで準備が完了です。CordovaプロジェクトとMonacaプロジェクトの互換性が向上していることでこのような流用が可能になっています。



ただし、Monacaプロジェクト上にこれらのファイルがあるのは良くないので、MonacaプロジェクトをGitに登録してこれをcloneし、そこにCordovaプロジェクトのファイルを入れつつ.gitignoreで元のMonacaプロジェクトには影響しないようにしておくのが良いかと思います。



Appiumのインストール



AppiumはiOS9への対応が進められているベータ版を使っています。





$ npm install appium@beta -g


Xcode7にするとインストール時に library not found for -lgcc_s.10.5 というエラーが発生する可能性があります。その時には下記コマンドを実行することで回避することができます(自己責任でお願いします)。





$ cd /usr/local/lib  & & sudo ln -s ../../lib/libSystem.B.dylib libgcc_s.10.5.dylib


Rubyライブラリのインストール



今回はRubyでテストコードを書いてみます。まずRubygemsでライブラリをインストールします。





$ gem install --no-rdoc --no-ri appium_lib


これで準備は完了です。



アプリをビルドする



テストを行うにあたって一旦ビルドを行う必要があります。Monacaアプリのディレクトリに移動して実行します。





$ monaca run ios


これでアプリが起動すればOKです。





アプリの起動



デモコードを書いてみる



まずはテストまでいかず、簡易的に動かしてみたいと思います。





require 'appium_lib'

capabilities = {
   app: "/path_to_app/TestApp.app",
   platformName: "iOS",
   deviceName: "iPhone Simulator",
   showIOSLog: true,
   defaultCommandTimeout:7200,
   debugLogSpacing: true
}

@wd = Appium::Driver.new(caps: capabilities)
Appium.promote_appium_methods Object
@wd.start_driver
begin
  @wd.texts
rescue
  @wd.start_driver
end
puts @wd.texts[0].text # HelloWorld!
@wd.texts[1].click
sleep(20)
driver_quit


appオプションは先ほどビルドしてできたアプリのパスを指定してください。driver_quitを実行するとすぐに終了してしまうので20秒のスリープを入れています。clickのようなイベントも使えますのでアプリの画面を遷移しつつ操作をDOMの状態をチェックすると言ったこともできます。



後はこのコードを実行すればOKです。以下はデモで動かしたところですが、実際にはもっと時間がかかります。







注意点



エラーについて



@wd.texts を実行したタイミングで





underlying webdriver instance does not support javascript (Selenium::WebDriver::Error::UnsupportedOperationError)


というエラーを起こす こと があります。その場合は再度実行してあげると直るようです。



ipaファイルでのテストについて



Appiumはipaファイルを使った実機テストにも対応しているのですが、うまくいきませんでした。そこでCordovaプロジェクトのスケルトンをベースにappを使ってシミュレータでテストを行っています。そのため今回の方法では実機では動きませんのでご注意ください。






ハイブリッドアプリのUIテストについては情報が多くなく、まだまだ手探りのところが多いのですが検証しつつ情報を出せればと思います。良いTipsがあればぜひ共有してください!

LocalKitを使っているなら自動テストも簡単。JavaScriptテストフレームワークまとめ

LocalKitを使うメリットの一つにテストフレームワークが容易に使えるようになるという点が挙げられます。そこで今回は開発したJavaScriptコードでユニットテストを実現するためのフレームワークを紹介します。



Jasmine: Behavior-Driven JavaScript





Jasmine



JasmineはRSecに似た記述のテスト文法をもったテストフレームワークとなっています。例えば次のように記述します。





describe("CallTracker", function() {
  it("tracks that it was called when executed", function() {
    var callTracker = new j$.CallTracker();

    expect(callTracker.any()).toBe(false);

    callTracker.track();

    expect(callTracker.any()).toBe(true);
  });

  it("tracks that number of times that it is executed", function() {
    var callTracker = new j$.CallTracker();

    expect(callTracker.count()).toEqual(0);

    callTracker.track();

    expect(callTracker.count()).toEqual(1);
  });
  :
});


expectで取得される値と期待される値とを比較する形になります。



QUnit





QUnit



QUnitは長く知られているユニットテストフレームワークになります。HTMLの結果画面を見たことがある方も多いのではないでしょうか。テストコードは次のように書きます。





QUnit.test("prettydate basics", function( assert ) {
  var now = "2008/01/28 22:25:00";
  assert.equal(prettyDate(now, "2008/01/28 22:24:30"), "just now");
  assert.equal(prettyDate(now, "2008/01/28 22:23:30"), "1 minute ago");
  :
});


元々jQueryから派生したツールと言うこともあり、jQuery必須となっています。



alexyoung/riotjs



riotjsはRubyのテストフレームワークRiotのJavaScript版です。テストは次のように記述します。





Riot.run(function() {
  context('basic riot functionality', function() {
    given('some simple equality tests', function() {
      asserts('a simple truth test should return true', true).isTrue();
      asserts('isNull is null', null).isNull();
    });

    given('another context', function() {
      asserts('equals should compare strings as expected', 'test string').equals('test string');
    });

    given('a context concerned with functions', function() {
      asserts('asserts() should allow functions to be compared', function() {
        return 'test string';
      }).equals('test string');
    });
  });

  given('yet another context', function() {
    asserts('equals should compare strings as expected', 'test string').equals('test string');
  });
});


ブラウザの他、RhinoやNodeでも動作します。



Unit testing framework for Javascript - Unit JS





Unit.js



Unit.jsもWebブラウザ、Nodeの両方で動作します。書き方が独特かも知れません。





// By example, provided by Express framework and other modules.
var req = {
  headers: {
    'content-type': 'application/json'
  }
};
test.object(obj).hasProperty('name');
test.object(obj).hasProperty('message', 'hello');
test.string(str).startsWith('Hello');
test.string(str).contains('world');
test.string(str).match(/[a-zA-Z]/);
test.value(req).hasHeader('content-type');
test.value(req).hasHeader('content-type', 'application/json');
// or
test.value(req).hasHeaderJson();


testというオブジェクトがあり、その下に変数の型を指定してプロパティや文字の長さ、正規表現などでチェックを行います。



nathansobo/screw-unit



Screw.UnitはBDDフレームワーク(ビヘイビア駆動開発)になります。RSpecに似た記述で、より仕様が分かりやすい文章を使ってテストを記述します。





describe("a nested describe", function() {
  var invocations = [];

  before(function() {
    invocations.push("before");
  });

  describe("a doubly nested describe", function() {
    before(function() {
      invocations.push('inner before');
    });

    it("runs befores in all ancestors prior to an it", function() {
      expect(invocations).to(equal, ["before", "inner before"]);
    });
  });
});


Protractor - end to end testing for AngularJS





Protractor



ProtractorはAngularJS用のE2Eテストフレームワークになります。専用ということもありますのでAngularJSアプリの場合はProtractorを使うのが良さそうです。





describe('angularjs homepage todo list', function() {
  it('should add a todo', function() {
    browser.get('https://angularjs.org');

    element(by.model('todoList.todoText')).sendKeys('write first protractor test');
    element(by.css('[value="add"]')).click();

    var todoList = element.all(by.repeater('todo in todoList.todos'));
    expect(todoList.count()).toEqual(3);
    expect(todoList.get(2).getText()).toEqual('write first protractor test');

    // You wrote your first test, cross it off the list
    todoList.get(2).element(by.css('input')).click();
    var completedAmount = element.all(by.css('.done-true'));
    expect(completedAmount.count()).toEqual(2);
  });
});


Mocha - the fun, simple, flexible JavaScript test framework





Mocha



Mochaはシンプルなテストフレームワークになっています。ブラウザでもNodeでも動作します。コードはJasmineなどに近いものになります。





var assert = require("assert")
describe('Array', function() {
  describe('#indexOf()', function () {
    it('should return -1 when the value is not present', function () {
      assert.equal(-1, [1,2,3].indexOf(5));
      assert.equal(-1, [1,2,3].indexOf(0));
    });
  });
});





今回紹介したフレームワークはWebブラウザやNodeで実行するものが多くなっています。そのため、MonacaアプリのJavaScript実装部分においてユニットテストができるでしょう。



もしOnsen UIを使われているならばProtractorは良い選択肢になるかと思います。品質高いアプリを目指し、しっかりとテストを作り込んでください。

Protractorでハイブリッドアプリを自動テスト!

こんにちは、吉田です。


アプリの開発にはテストがつきものですよね。
納品前に最終的なテストを行うのはもちろんですが、それに加えて開発中も気軽にテストを実行できる環境があれば、既存コードのリファクタリングや使用しているフレームワークのバージョンアップなども安心して行えます。


内部のロジックをクラスやメソッド単位で内側からテストするユニットテストの自動化は行われている場合も多いかと思いますが、アプリの場合は画面遷移や要素の表示・挙動が占めるウエイトもかなり大きいので、「Aのボタンを押したらBの画面に遷移し、Cという要素が表示されている」といったような外側からのテストも大事になってきます。
その辺りのテストは手動で行う場合もあるかもしれませんが、フレームワークのバージョンアップやリファクタリングのたびに手動で全ページを表示して動作確認を行うのはつらすぎる。。
外側からのテストも自動化したい・・!!


というわけで今回は
「Cordova + AngularJS + Onsen UI でハイブリッドアプリを作り、それをProtractorで外側から自動テストする」
という内容をご紹介してみたいと思います!


※ちなみに今回アプリを実行するのは実機ではなくブラウザなので、実機で行う厳密な表示テストに替わるものではありませんが、JavaScriptだけで手軽に画面遷移や要素の有無を確認する自動テストを書きたい!という時には役に立つ場面があるかなと思います。


■Cordova + Onsen UIでテスト対象のアプリを作成


まずはテスト対象となる簡単なアプリを作成します。
今回私の記事ではテストを行う部分がメインですので、対象アプリの作成については順を追ったコマンドだけ羅列していきます。
(アプリ作成の段階については別の記事:AngularJS + Onsen UIで始めるPhoneGapアプリケーションで詳しく解説されていますので、ご興味のある方はぜひどうぞ!)


ではターミナルを開いてアプリを作っていきましょう!




# cordovaコマンドが入っていない方のみ
sudo npm install -g cordova

# cordovaアプリの作成
cordova create test-app com.example.testapp TestApp
cd test-app
cordova platform add ios

ここまでで、Cordovaアプリのファイル群ができ、iOSシミュレータや実機で動くようになりました。
シミュレータや実機で動かせる環境がある場合は、cordova run iosで実行できるはずです。


しかし今回の自動テストはPCで立ち上げるブラウザを対象に行いますので、iOSシミュレータや実機の環境がない方でも実行できます!
ではさっそくブラウザで動作確認してみましょう。





cordova serve ios


"Static file server running on port 8000 (i.e. http://localhost:8000)"
というメッセージを確認して、http://localhost:8000 にアクセス。








こんなかんじの画面が出ればOK!
cordova serveをCtrl-Cで停止して、今度は表示する内容をOnsen UIのテンプレートに差し替えてみましょう。
wgetコマンドが入っていない場合は、こちらのzipファイルを落として解凍して手動で差し替えても問題ありません。





wget http://ja.onsenui.io/OnsenUI/project_templates/onsen_master_detail.zip
unzip onsen_master_detail.zip -d onsen
rm -r www
cp -r onsen/www .
rm -r onsen
rm onsen_master_detail.zip


もう一度実行してみます。





cordova serve ios


http://localhost:8000/ios/www/ でこんな画面が表示されて動いていればOK!








これで、テスト対象アプリの準備が出来ました。


■自動テスト環境の準備


今回はブラウザを自動で操作して外側からのテスト、いわゆるエンドツーエンドテストを行ってみたいと思います。
AngularJSがエンドツーエンドテストツールとして推奨している、Protractorというライブラリを使ってみましょう!





npm install protractor # Protractorのインストール
node_modules/.bin/webdriver-manager update # Seleniumツールのインストール


準備ができたら、プロジェクトのルートディレクトリ(test-app/)直下にテスト用のディレクトリを作ります。





mkdir spec
cd spec


ここに、conf.jsというProtractorの設定ファイルを作ります。
内容は以下のとおり。





exports.config = {

  // seleniumサーバーのURL
  seleniumAddress: 'http://127.0.0.1:4444/wd/hub',

  // テストコードを書いたファイル
  specs: [
      'spec.js'
  ],

  // 使用するブラウザ
  capabilities: {
    'browserName': 'chrome'
  }
};


テストを書くファイルは、同じくspecディレクトリの中にspec.jsという名前で作成します。
JSテストフレームワークであるJasmineを使用するので、Jasmineの書き方でテストを記述します。





describe('エンドツーエンドテスト', function() {

    it('最初の画面のテスト', function() {

        // cordova serveのURLを開く
        browser.get('http://localhost:8000/ios/www/');

        // <ons-navigator>タグが表示されている
        expect($('ons-navigator').isDisplayed()).toBe(true);

        // <ons-toolbar>の中のテキストが"Master Detail"と一致する
        expect($('ons-toolbar').getText()).toEqual('Master Detail');
    });
});


specディレクトリの中にconf.jsとspec.jsが準備できましたか?
ではいよいよ実行してみましょう!!


プロジェクトのルートディレクトリ(test-app/)を開いたターミナルの画面を3つ用意してください。
それぞれで





node_modules/.bin/webdriver-manager start # 1つ目のターミナルでSeleniumサーバーを起動
cordova serve ios # 2つ目のターミナルでCordovaアプリをブラウザで実行
node_modules/.bin/protractor spec/conf.js # 3つ目のターミナルでテスト実行


を上から順に実行すると・・・
勝手にChromeが立ち上がって、テストが実行されましたでしょうか?
テストを実行したターミナルに、


1 test, 2 assertions, 0 failures


と表示されていればめでたく成功です!


■テストを増やす


やっと自動でテストを実行できるようになったので、ページ遷移をテストしてみましょう!
リストをクリックして遷移先のページの内容をチェックしていく処理を追記します。





describe('エンドツーエンドテスト', function() {

    it('最初の画面のテスト', function() {

        // cordova serveのURLを開く
        browser.get('http://localhost:8000/ios/www/');

        // <ons-navigator>タグが表示されている
        expect($('ons-navigator').isDisplayed()).toBe(true);

        // <ons-toolbar>の中のテキストが"Master Detail"と一致する
        expect($('ons-toolbar').getText()).toEqual('Master Detail');
    });

    // ----------------- ここから追記 ---------------------

    it('画面遷移 リスト1つ目', function() {
        // リストの1つ目をクリック
        $$('ons-list-item').get(0).click();

        // 画面が変わるのを待つ(1秒)
        waits(1000);
        runs(function() {
            expect($('ons-list-header').getText()).toEqual('Item Information');
            expect($('ons-list-item strong').getText()).toEqual('Item 1 Title');
            $('ons-back-button').click(); // Backボタンをクリック
        });
        waits(1000);
    });

    it('画面遷移 リスト2つ目', function() {
        // リストの2つ目をクリック
        $$('ons-list-item').get(1).click();

        // 画面が変わるのを待つ(1秒)
        waits(1000);
        runs(function() {
            expect($('ons-list-header').getText()).toEqual('Item Information');
            expect($('ons-list-item strong').getText()).toEqual('Another Item Title');
            $('ons-back-button').click(); // Backボタンをクリック
        });
        waits(1000);
    });

    it('画面遷移 リスト3つ目', function() {
        // リストの3つ目をクリック
        $$('ons-list-item').get(2).click();

        // 画面が変わるのを待つ(1秒)
        waits(1000);
        runs(function() {
            expect($('ons-list-header').getText()).toEqual('Item Information');
            expect($('ons-list-item strong').getText()).toEqual('Yet Another Item Title');
            $('ons-back-button').click(); // Backボタンをクリック
        });
        waits(1000);
    });
});


合計4つの画面の遷移と、遷移先ページのテキストのテストは無事に成功しましたでしょうか?
成功するとこんな感じになります!(アニメーションGIF)



画面遷移のときは、画面の切り替えを待つためにclickのあとにwaitsを挟むのがコツです!
ブラウザの画面が自動で動いていくのは面白いですね!


あとは今の冗長なテストコードをリファクタリングしたり、実装に合わせてテストも成長させていきましょう!


■テストの記法について知りたい場合


describe, it, expectなどはJasmineのメソッド
$, getText, clickなどはProtractorのメソッドになります。
どちらも今回使った他にまだまだ機能があり、特にProtractorはAngularJSが推奨しているだけあって、たとえばng-modelやng-bindの値を使って要素を指定することなどもできます。


また、今回はボタンでの画面遷移と要素の確認だけでしたが、他にもポップアップしたアラートの操作やフォームへの入力、スクリーンショットをとることなどもできますので、その辺りも気になる方は以下のドキュメントをご参照ください!


Jasmine(1.3) ドキュメント
Protractor ドキュメント


もしくはリクエストいただければその辺りまで踏み込んだブログを書くかもしれません!


■おわりに


Cordovaアプリの画面遷移チェック自動化について、私が試してみた方法をご紹介させていただきました。


ちなみに今回の記事の手法をひと通り試し終わったあとに見つけたのですが、実機で自動エンドツーエンドテストができるappiumというオープンソースフレームワークもあるようです。
とても気になっているのでそちらも是非試してみたいです!


他にも色々な方法・ノウハウがあるかと思いますので、是非教えていただけると嬉しいです!

REST APIのテストをFrisbyで自動化する


 どうも、中本(特に冷やし五目味噌タンメン+バター)にハマっている高橋です!

 最近のアプリケーション開発といえば、フロントエンドはサーバサイドが準備したAPI経由でデータを取得したり保存したりという構成が人気のようです。そこで「API、ちゃんと動いてるんかなぁ?」というテストを書いて、実際にリクエスト&レスポンスで検証してみようと思います。

 今回テスティングフレームワークとして使用する Frisby(フリスビー) は簡単に書けて高速に動作するというのが持ち味の REST API のテスティングフレームワークです。投げて返ってくるFrisbeeと掛けているのでしょうか?これドヤ顔で言われるとちょっと腹立ちますが、こういうネーミングセンスには関心させられます。笑


◯インストール



 今回は「frisbytest」というディレクトリ内で作業をしていきたいと思います。
 コンソールを起動したら以下のコマンドを実行してFrisbyを扱う準備を進めます。




mkdir frisbytest
cd frisbytest
npm install frisby
npm install -g jasmine-node



 jasmine-nodeもインストールしておきました。
 これで「*spec.js」にマッチするファイルをテストしてくれます。
 詳しくは https://github.com/mhevery/jasmine-node#usage

◯テストをする



 次はテストするために「first_spec.js」というファイルにテストを書いていきたいと思います。またテストコードは「spec」というディレクトリ以下に作っていくのが慣習になので、それに倣って進めます。テスト対象は偶然見つけたアニメマップ(http://animemap.net/pages/api/)という公開APIを使ってみます。




mkdir spec
cd spec
vim first_spec.js



first_spec.js





var frisby = require('frisby');
frisby.create('GET 東京都の番組データ')
    .get('http://animemap.net/api/table/tokyo.json')
    .expectJSON({
        response: {
            item: [
                {
                    title: 'pupa(ピューパ)'
                }
            ]
        }
    })
    .toss();


(コピペしても行番号はコピーされません^^/)

さっそく実行してみましょう!




jasmine-node .






yuya[/Users/yuya/work/frisbytest/spec]% jasmine-node .
F

Failures:

  1) Frisby Test: GET 東京都の番組データ
	[ GET http://animemap.net/api/table/tokyo.json ]
   Message:
     Error: Expected string 'とある飛空士への恋歌' to match string 'pupa(ピューパ)' on key 'title'
   Stacktrace:
     Error: Expected string 'とある飛空士への恋歌' to match string 'pupa(ピューパ)' on key 'title'
    at _jsonContains (/Users/yuya/work/frisbytest/node_modules/frisby/lib/frisby.js:1209:17)
    at _jsonContains (/Users/yuya/work/frisbytest/node_modules/frisby/lib/frisby.js:1188:9)
    at _jsonContains (/Users/yuya/work/frisbytest/node_modules/frisby/lib/frisby.js:1188:9)
    at _jsonContains (/Users/yuya/work/frisbytest/node_modules/frisby/lib/frisby.js:1188:9)
    at jasmine.Matchers.toContainJson (/Users/yuya/work/frisbytest/node_modules/frisby/lib/frisby.js:1124:12)
    at null.<anonymous> (/Users/yuya/work/frisbytest/node_modules/frisby/lib/frisby.js:676:24)
    at null.<anonymous> (/Users/yuya/work/frisbytest/node_modules/frisby/lib/frisby.js:1033:43)

Finished in 0.276 seconds
1 test, 1 assertion, 1 failure, 0 skipped



 あれ、テストがこけてしまいましたね.....と見せかけてこれで正解です!
 基本的にテストは赤(失敗)で始めるのが鉄則ですのでこれでOK!大丈夫!安心して!間違いない!一切問題なし!心配ご無用なのです!!

 「Error: Expected string 'とある飛空士への恋歌' to match string 'pupa(ピューパ)' on key 'title'」というメッセージがありますので、改めて期待値を修正します。




var frisby = require('frisby');

frisby.create('GET 東京都の番組データ')
    .get('http://animemap.net/api/table/tokyo.json')
    .expectJSON({
        response: {
            item: [
                {
                    title: 'とある飛空士への恋歌'
                }
            ]
        }
    })
    .toss();



さて、これでもう一度 「jasmine-node .」 を実行してみましょう!




yuya[/Users/yuya/work/frisbytest/spec]% jasmine-node .
.

Finished in 0.886 seconds
1 test, 2 assertions, 0 failures, 0 skipped



 よし!緑(成功)になりましたね!おめでとうございます!!

 というようにたったのこれだけでWebAPIのテストが出来ました。
この要領で全APIのテストケースを作成すれば、サーバ側でなんかしらの修正をしても、いつでも全てのAPIの動作チェックが出来ますので、これはもうなんだか幸せになれそうな気がしてきますね!

◯学習



 基本的な使い方を覚えたら、次はFrisbyを効率的に学習する方法についてです。

 Frisbyをインストールした時にサンプルコード(node_modules/frisby/examples/)が付いてきますので、そのコードを眺めるのが手っ取り早いかと思います。

 また、Frisbyのソースコード(node_modules/frisby/lib/frisby.js)は1300行程度ですので、これを読んでしまうのもオススメです。というのも、まだドキュメント化されていないメソッドがあったりで情報が少ないためです。でもなんにせよソース読んだほうがスッキリしますよね。

◯Frisbyでテストを書くコツ



 コツというか経験をユースケースにして重要な機能を紹介します。これを覚えたらFrisbyでテストを書くのは困らないかなぁと思います。

・リクエストヘッダの一括設定と個別追加&除去





frisby.globalSetup({
    request: {
        headers: {'X-CUSTOM-HEADER-VALUE': 'カスタムヘッダです'}
    }
});

frisby.create('カスタムヘッダありで実行A')
    .... // いろんな処理
    .toss();

frisby.create('カスタムヘッダありで実行B')
    .... // いろんな処理
    .toss();

frisby.create('カスタムヘッダあり + 追加で実行')
    .addHeader('X-CUSTOM-HEADER-MORE-VALUE', 'さらに追加')
    .... // いろんな処理
    .toss();

frisby.create('カスタムヘッダなしで実行')
    .removeHeader('X-CUSTOM-HEADER-VALUE')
    .... // いろんな処理
    .toss();



・GET以外のメソッド(POST、PUT、DELETE)を使う





frisby.create('POSTメソッドで実行')
    .post('http://localhost/something', {hoge: 'huga'})
    .... // いろんな処理
    .toss();

frisby.create('PUTメソッドで実行')
    .put('http://localhost/something/10', {hoge: 'hugahuga'})
    .... // いろんな処理
    .toss();

frisby.create('DELETEメソッドで実行')
    .delete('http://localhost/something/10')
    .... // いろんな処理
    .toss();

frisby.create('HEADメソッドも対応してる')
    .head('http://localhost/something/10')
    .... // いろんな処理
    .toss();

frisby.create('PATCHメソッドも対応してる')
    .patch('http://localhost/something/10', {hoge: 'hugahuga'})
    .... // いろんな処理
    .toss();



・404ページかどうか(ステータスコード)をチェックする





frisby.create('404 ページかどうか')
    .get('https://www.google.co.jp/hogehoge')
    .expectStatus(404)
    .toss();



・ヘッダーを検証する(完全一致)





frisby.create('ヘッダーを検証する')
    .get('https://www.google.co.jp')
    .expectHeader('content-type', 'text/html; charset=Shift_JIS')
    .toss();



・ヘッダーを検証する(部分一致)





frisby.create('ヘッダーを検証する')
    .get('https://www.google.co.jp')
    .expectHeaderContains('content-type', 'text/html')
    .toss();



・依存性のあるテストの対応(レスポンスがJSONのみ対応)





frisby.create('テストA')
    .get('http://animemap.net/api/table/tokyo.json')
    .afterJSON(function (data)
    {
        frisby.create('テストAの結果に依存するテストB')
            .get('http://localhost/something', {data: data})
            .... // いろんな処理
            .toss();
    })
    .toss();



・レスポンスヘッダやレスポンスボディを確認する





frisby.create('他にも色々と確認できるメソッド')
    .get('http://animemap.net/api/table/tokyo.json')
    .inspectHeaders()
    .inspectBody()
    .inspectJSON() // inspectBody()をJSONパースしたもの
    .inspectStatus()
    .inspectRequest()
    .inspectResponse()
    .toss();



タイムアウトの制限時間を設定する





frisby.create('タイムアウトを10秒に設定(デフォルトは5000ミリ秒)')
    .timeout(10000)
    .get('http://animemap.net/api/table/tokyo.json')
    .toss();



Basic認証がある場合





frisby.create('Basic認証を突破!')
    .auth('authname', 'authpassword')
    .get('http://animemap.net/api/table/tokyo.json')
    .toss();




◯まとめ



 今回のサンプルソースではGETメソッドしか試していませんが、実際のアプリケーションではPUTメソッドやDELETEメッドも当然実装されますから、これらをテストするために毎回cURL等で確認するのは手間だと思います。状況にもよるでしょうが、何回か実行する可能性があるのなら自動化しておいて損はないでしょう。

 最後に、これらは成果物にはならないかもしれませんが、心の健康と変更に対する勇気を得るためにもテストコードを書いてみてはいかがでしょうか?

それでは!

Selenium WebDriverでマウス操作 & 処理待機

はじめに



Selenium WebDriverを使えばブラウザを自動操作できます。WebDriverは単純なクリックから複雑なマウス操作や非同期処理の確認などもできます。最近のWebサイトやWebシステムではAjaxが多用されていたり、ドラッグ&ドロップを使用していたりと、結構複雑です。そんな場合のSelenium WebDriverの簡単な使い方をご紹介します。

Selenium WebDriverとは?」「どうやって使うのか?」と思った方は、簡単・便利、ブラウザの自動操作!~Selenium WebDriver~をご一読下さい。


複数操作



マウスで複数の操作を一連の動きとして実行する場合、どのようにするのか、簡単にやってみます。アシアル・ホームページのトップ画面上部に、下の画像の部分があります。


この部分(スライダー)をマウスでドラッグすると、その下の構築事例や会社情報が切り替わります。このドラッグをSelenium WebDriverで実行してみます。以下のコードを実行すると、スライダーが右に移動し、パネル部分が変化します。


package jp.co.asial.test;

import java.util.concurrent.TimeUnit;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.interactions.Action;
import org.openqa.selenium.interactions.Actions;

public class AsialHpSliderTest {

    public static void main(String[] args) {
        WebDriver driver = new FirefoxDriver();
        driver.get("http://www.asial.co.jp");
        driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
        
        // 興味のある技術分野の選択
        Action action = new Actions(driver)
            .clickAndHold(driver.findElement(By.cssSelector(".ui-slider-handle")))
            .moveByOffset(100, 0)
            .release()
            .build();
        action.perform();
    }
}

実際にマウスでスライダーを移動させる場合、マウス・ダウン、ドラッグ、リリースの3つの処理を実行します。このようにマウス操作を同時に複数行う場合、SeleniumではActionsクラスを使います。このクラスは、マウスの基本操作を実行するメソッドを多数保持しています。上記の通りBuilderパターンを使ってインスタンスを作成し、peform()メソッドで一連の処理を実行します。

単純なドラッグ&ドロップのみであれば、以下のようにも記述できます。dragAndDropBy()メソッドはclickAndHold()、moveByOffset()、release()を組み合わせたメソッドです。


Action action = new Actions(driver)
    .dragAndDropBy(driver.findElement(By.cssSelector(".ui-slider-handle")), 100, 0)
    .build();
action.perform();



アニメーション終了確認



先のコードではブラウザを開いたまま処理を終えました。本当はアニメーション終了とともにブラウザを閉じたいのですが、素直には実行できません。例えば、上記コードの最後にdriver.quit()を入れると、アニメーション前にブラウザを閉じてしまいます。ドラッグ&ドロップを終えるとすぐに次の処理を実行してしまうからです。

意図通りに動かしたい場合、処理を中断(待機)し、適切なタイミングで再開する必要があります。こういう場合、WebDriverWaitクラスを用いて待機処理を実装します。

上記パネルの更新は次のようになっています。

  1. パネル要素を画面外へ移動する
  2. 新しい要素を追加する
  3. 古い要素を削除する
  4. 新しいパネルを画面内へ移動する

このパネル更新を確認するためには、①パネルが動き出すまで待ち、②新しい要素が画面内の定位置に来るまで待つ、ことが必要です。これは以下のように実装できます。


package jp.co.asial.test;

import java.util.concurrent.TimeUnit;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.interactions.Action;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.WebDriverWait;

public class AsialHpSliderTest {

    public static void main(String[] args) {
        WebDriver driver = new FirefoxDriver();
        driver.get("http://www.asial.co.jp");
        driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
        
        // スライダーを移動
        Action action = new Actions(driver)
            .dragAndDropBy(driver.findElement(By.cssSelector(".ui-slider-handle")), 100, 0)
            .build();
        action.perform();

        // 画面が動き出すまで待つ: 10秒間、50ms間隔で確認
        new WebDriverWait(driver, 10, 50).until(new ExpectedCondition<Boolean>() {
            @Override
            public Boolean apply(WebDriver driver) {
                final WebElement element = driver.findElement(By.cssSelector(".topCardList > article:first-child"));
                return !element.getCssValue("top").equals("0px");
            }
        });

        // 最初の要素の位置が確定するまで待つ: 10秒間、100ms間隔で確認
        new WebDriverWait(driver, 10, 100).until(new ExpectedCondition<Boolean>() {
            @Override
            public Boolean apply(WebDriver driver) {
                final WebElement element = driver.findElement(By.cssSelector(".topCardList > article:nth-last-child(21)"));
                return element.getCssValue("top").equals("0px");
            }
        });

        driver.quit();
    }
}

WebDriverWaitコンストラクタの第二、第三引数にはそれぞれ最大待ち時間[秒]と確認間隔時間[ミリ秒]を渡します。そして、WebDriverWaitオブジェクトのuntil()メソッドへExpectedConditionオブジェクトを渡すことで処理を待機します。この仕組みはAjaxなどの非同期処理を確認する際には多用します。ちなみに、アシアル・サイトのパネル操作でもAjax通信を行っています。

実際のところ、上記のコードはたまに失敗してしまいます。2番目の待機で、最初の要素を上手く特定できないことがあるためです。Webサイトの自動テスト等を考える場合、HTMLの構造とJavaScriptの使い方も考える必要がありそうです。適切にIDを割り当てる、Ajaxが終了したら何らかの属性を変化させるなど、ちょっとした処理を入れておくと自動操作もかなり楽になります。もちろん、複雑にならないようなバランス感覚も重要です。


おわりに



マウス操作や処理待機の扱いになれれば、Selenium WebDriverを使ったブラウザ自動操作もかなり楽になります。その他にもダブルクリックや、キーボード入力なども扱えます。かなり万能に操作やテストを実行できますので、ぜひ試してみて下さい。

簡単・便利、ブラウザの自動操作!~Selenium WebDriver~

はじめに



今回はSelenium WebDriverをご紹介します。ブラウザ操作を自動化する際には最適な仕組みです。Webシステムのend-to-endテストを自動化する際には、ブラウザ操作が必要になることがあります。そんな時にSelenium WebDriverはとても便利です。


Selenium



Seleniumとは、ブラウザをプログラムで動かすフレームワークです。この仕組みを使うことで、ユーザーテストなど、様々な処理を自動化できます。現在のところ、Seleniumは以下のWebブラウザを制御できます(公式サイト)。


実際に使用する際には、以下の2つの仕組みのどちらかを使用します。

Selenium IDEFirefoxのアドオンです。Firefox上でユーザー操作を記録します。この処理を再度実行したり、WebDriverのコードへ変換できます。バグを再現させる場合など、手軽に使えて非常に便利です。

Selenium WebDriverとは、ブラウザの拡張機能やOSネイティブの機能を使ってブラウザを操作するライブラリ群、もしくはその仕組みのことです。プログラムに記述した通りにブラウザを操作できます。JavaPythonRubyC#のいずれかを使用します(公式提供)。ブラウザを使用した自動テストを詳細に作成する場合などに便利です。

今回は、ブラウザ制御を全て自動化できるSelenium WebDriverを使ってみます。Javaを使って、以下の2つのことをしてみましょう。

  • PC上でのブラウザ操作
  • Android端末(実機)上でのブラウザ操作

画面遷移、JavaScriptのクリックイベント実行、JUnitとの組み合わせなども試してみます。


プロジェクト作成



まずは、Javaのパッケージを下記サイトからダウンロードします。2013年7月5日現在では、selenium-java-2.33.0.zipが最新版のようです。

http://docs.seleniumhq.org/download/

ダウンロードしたファイルを展開すると、無意味に深い階層が現れます。最後のselenium-2.33.0ディレクトリをそのままeclipseインストールディレクトリのdropinsディレクトリへ移動しておきます。


selenium-2.33.0
└ selenium-2.33.0
   └ selenium-2.33.0
       ├ libs
       ├ selenium-java-2.33.0.jar
       └ selenium-java-2.33.0-srcs.jar

次に、プロジェクトを作成します。Javaプロジェクトをeclipseで作成し、SeleniumのJARファイルをプロジェクトへ追加します。JARの追加方法は以下の通りです。

  1. プロジェクトを右クリック
  2. Java Build Pathを選択
  3. Add External JARsをクリック
  4. eclipse/dropins内のselenium-java-2.33.0.jarとlibs内の全てのjarを追加

ここまで出来たら、後はコードを書いてブラウザを動かしてみるだけです!

[補足] プロジェクト自体にjarを追加することもできます。以下のようにすると、jarがプロジェクトに追加され、プロジェクト単体で処理を完結できます。

  1. プロジェクトディレクトリへselenium-2.33.0ディレクトリをコピー
  2. プロジェクトを右クリック
  3. Java Build Pathを選択
  4. Add JARsをクリック
  5. プロジェクト内のselenium-java-2.33.0.jarとlibs内の全てのjarを追加


PCブラウザ操作



ブラウザの起動・終了



まずは、ブラウザを単純に開いて閉じることをしてみましょう。ここではアシアルのサイトを見てみます。今回はFirefoxを使います。


package jp.co.asial.test;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;

public class AsialHpTest {

    public static void main(String[] args) {
        WebDriver driver = new FirefoxDriver();
        driver.get("http://www.asial.co.jp");
        driver.quit();
    }
}

WebDriverのget()メソッドで画面を開き、quite()で閉じます。後は、Java Applicationとして実行します。すると、しばらくしてFirefoxが立ち上がり、アシアルのサイトが表示され、すぐにブラウザが閉じると思います。起動するまでに若干時間がかかります。

画面遷移



せっかくブラウザを開いたのだから、何らかの操作をしてみます。画面のリンクをクリックし、別の画面へ遷移してみます。下記のコードでは、各処理の間に1秒間処理を止めています。これを入れないと、処理が速すぎて何をしているかよく分からないためです。もちろん、テストを運用する時には外します。


package jp.co.asial.test;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;

public class AsialHpTest {
    public static void main(String[] args) {
        WebDriver driver = new FirefoxDriver();
        driver.get("http://www.asial.co.jp");
        sleep(1000);
        driver.findElement(By.cssSelector("ul.globalMenuNormal li:first-child")).click();
        sleep(1000);
        driver.quit();
    }
    
    private static void sleep(int microtime) {
        try {
            Thread.sleep(microtime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

ここでは、WebDriverのfindElement()メソッドとBy.cssSelector()メソッドを使ってCSSセレクタから対象要素(WebElement)を取得しています。そして、その要素(Aタグ)をクリックしています。WebDriverがこのクリックをブラウザ上で実行することで、実際に画面遷移が行われます。JavaScriptを使えるエンジニアであれば、サクサク記述できると思います。

ちなみに、WebDriverのfindElement()メソッドでは、WebElementインタフェースを実装したオブジェクトを取得できます。そのオブジェクトを使えば、要素が現在見えているのか否か、どこに表示されているか、要素の表示サイズ、などを取得できます。例えば以下のように記述できます。



WebElement element = driver.findElement(By.cssSelector("ul.globalMenuNormal li:last-child"));
System.out.println("displayed = " + String.valueOf(element.isDisplayed()));
System.out.println("enabled   = " + String.valueOf(element.isEnabled()));
System.out.println("position  = " + element.getLocation());
System.out.println("dimension = " + element.getSize());
System.out.println("font-size = " + element.getCssValue("font-size"));


JavaScript実行



次に、JavaScriptのクリックイベントを実行してみます。アシアルのサイトでは、サイト上部の画像の両脇のボタンを押すことで、画像を左右に移動して変更できます。プログラムからこの操作をしてみたいと思います。


package jp.co.asial.test;

import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.remote.RemoteWebDriver;

public class AsialHpTest {
    public static void main(String[] args) {
        RemoteWebDriver driver = new FirefoxDriver();
        
        driver.get("http://www.asial.co.jp");
        sleep(1000);
        WebElement element = driver.findElement(By.cssSelector("ul.flex-direction-nav a.prev"));
        
        for (int i=0; i<5; i++) {
            driver.executeScript("arguments[0].click();", element);
            sleep(1000);
        }
        
        driver.quit();
        
    }
    
    private static void sleep(int microtime)
    {
        // 省略
    }
}

ポイントは、RemoteWebDriverのexecuteScriptメソッドを使用している点です。RemoteWebDriverはWebDriverインタフェースを実装したクラスです。さらに、RemoteWebDriverはJavascriptExecutorインタフェースも実装しており、ここからJavaScriptを処理できます。FirefoxDriverはRemoteWebViewのサブクラスです。

上記コードにて、element(WebElementオブジェクト)のclick()メソッドを実行するとエラーとなります。

もちろん、WebDriverインタフェースの形でも同様のことをできます。


public class AsialHpTest {
    public static void main(String[] args) {
        WebDriver driver = new FirefoxDriver();
        JavascriptExecutor executor = (JavascriptExecutor)driver;
        
        ...
        
        for (int i=0; i<5; i++) {
            executor.executeScript(...);
        }
        
        ...
    }
    
    ...
}


スクリーンショット



スクリーンショットも軽々と撮れます。テスト結果の検証物件として納品する場合に便利そうです。


package jp.co.asial.test;

import java.io.File;
import java.io.IOException;

import org.apache.commons.io.FileUtils;

import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.firefox.FirefoxDriver;

public class AsialScreenShot {
    public static void main(String[] args) {
        WebDriver driver = new FirefoxDriver();
        driver.get("http://www.asial.co.jp");
        
        try {
            FileUtils.copyFile(
                    ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE),
                    new File("path/to/file"));
        } catch (WebDriverException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        driver.quit();
    }
}


JUnit



PCブラウザ操作の最後に、JUnitと組み合わせて使用してみます。下記コードをRun As => JUnit Testから実行します(Eclipse JUnit Launcher)。JUnitがテストケースを順番に実行してくれます。また、Assertを使用して画面URLや各要素を確認することもできます。ここまで来ると、Selenium WebDriverを使えばテスト自動化もサクサクできそうな感じがします。



package jp.co.asial.test;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;

public class AsialHpJUnitTest {
    private WebDriver driver;

    @Before
    public void beforeTest() {
        driver = new FirefoxDriver();
    }

    @After
    public void afterTest() {
        sleep(1000); // 1秒後にブラウザを閉じる(動きを確認するため)
        driver.quit();
    }
    
    // トップ画面を開くだけ
    @Test
    public void open() {
        driver.get("http://www.asial.co.jp");
    }
    
    // スクール画面へ遷移する
    @Test
    public void moveToSchoolPage()
    {
        driver.get("http://www.asial.co.jp");
        driver.findElement(By.cssSelector("ul.globalMenuNormal li:last-child")).click();
        Assert.assertEquals("http://www.asial.co.jp/school/", driver.getCurrentUrl());
    }
    
    private void sleep(int microtime)
    {
        try {
            Thread.sleep(microtime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}



Androidブラウザ操作



最後にAndroid端末上でブラウザを操作してみます。AndroidDriverを使うことで、自分達のサイトがAndroidブラウザ上で正しく動いているか、自動化されたend-to-endテストを実行できます。多種多様なAndroid端末が出回っているため、これが出来るととても楽になりそうです。

Androidでのブラウザ操作もしくはテストには2つの方法があります(参考)。


前者は、様々な端末で同じテストを実行したい場合に使うと便利です。後者は、既にAndroidテストフレームワークを使用ていて、かつ特定の端末で素早いテストを実行したい場合に便利です。今回は前者の方法でブラウザ操作を実行してみたいと思います。

PC・端末の設定



リモートサーバといっても、server=Android端末、client=自分のPCという構成です。Android端末とPCをそれぞれ設定していきます。端末の設定はとても簡単で、3つのことをするだけです。

  • 端末をデバッグ可能モードでPCへUSB接続
  • Androidアプリ「WebDriver」をAndroid端末へインストール
  • WebDriverアプリを起動

Androidアプリ「WebDriver」がリモートサーバーとなるアプリケーションです。このアプリを使用してブラウザ操作を行います(正確にはWebView)。以下のサイトから、android-server-2.32.0.apkをダウンロードしてインストールします。

https://code.google.com/p/selenium/downloads/list

現在のところ、Android2.3.x、3.x、4.0.x以上で使用可能です。インストールが終わったら、アプリを起動します。後はUSBでPCへ接続し、eclipseから認識できていれば大丈夫です。

次にPCの設定を行います。PCの設定は以下の2つです。

  • Android SDK のインストール
  • ABD用USBドライバのインストール
  • ポートフォワード設定

Android SDKとUSBドライバのインストールはSDK Managerから実行できます。ポートフォワード設定を行うには、端末のデバイスIDが必要です。上記のように端末をPCに接続した状態で、eclipseのDDMSからデバイスIDを確認できます。もしくはadbコマンドを以下のように実行すると、PCに接続している端末のデバイスID(16桁)を取得できます。


$ adb devices

ポートフォワードの設定は次の通りです。[DeviceId]の部分に端末のデバイスIDを入れます。


$ adb -s [DeviceId] forward tcp:8080 tcp:8080

これで準備は終わりです。

ブラウザ操作



では、Android端末のブラウザ操作を行ってみます。アシアルのサイトを表示し、上部リンクの位置等をコンソールに出力します。その後、200px上部へスクロールを実施。先のリンクの情報を再度コンソールに出力(位置情報が変わっている)。最後に「セミナー&スクール」の画面へ遷移します。


package jp.co.asial.test;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.android.AndroidDriver;

public class AsialHpAndroidTest {
    public static void main(String[] args) {
        AndroidDriver driver = new AndroidDriver();
        
        driver.get("http://www.asial.co.jp");
        check(driver, "ul.globalMenuNormal li:last-child");
        sleep(1000);
        
        driver.getTouch().scroll(0, 200);
        check(driver, "ul.globalMenuNormal li:last-child");
        sleep(1000);
        
        driver.findElementByCssSelector("ul.globalMenuNormal li:last-child").click();
        
        System.out.println("Page title: " + driver.getTitle());
        System.out.println("Page URL: " + driver.getCurrentUrl());
    }
    
    private static void check(final WebDriver driver, final String selector){
        final WebElement element = driver.findElement(By.cssSelector(selector));
        System.out.println(selector);
        System.out.println("displayed = " + String.valueOf(element.isDisplayed()));
        System.out.println("enabled   = " + String.valueOf(element.isEnabled()));
        System.out.println("position  = " + element.getLocation());
        System.out.println("dimension = " + element.getSize());
        System.out.println("font-size = " + element.getCssValue("font-size"));
        System.out.println("");
    }
    
    private static void sleep(int microtime)
    {
        try {
            Thread.sleep(microtime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

コンソールへの出力結果は次のようになります。positionのy座標が200減っていることがわかります。


ul.globalMenuNormal li:last-child
displayed = true
enabled   = true
position  = (168, 80)  <== スクロール前
dimension = (136, 32)
font-size = 13px
 
ul.globalMenuNormal li:last-child
displayed = true
enabled   = true
position  = (168, -120)  <== スクロール後
dimension = (136, 32)
font-size = 13px
 
Page title: PHP, HTML, Linux 講座 : セミナー&スクール | アシアル株式会社
Page URL: http://www.asial.co.jp/school/

また、端末情報も次のように取得できます。


driver.getCapabilities().getBrowserName(); // android
driver.getCapabilities().getVersion(); // API Level



おわりに



Selenium WebDriverをPCブラウザとAndroidブラウザの両方に対して使ってみました。簡単に使用でき、ブラウザ内の操作や要素の情報も容易に取得できます。たまにブラウザ操作が失敗することが玉に瑕ですが、テストの自動化には最適だと思います。軽くでも試してみると、開発の幅が広がると思います。

ratproxyとSelenium IDEを組み合わせたWebアプリケーションテスト

今回は、統合テストなどで利用できる便利ツール「Selenium IDE」と「ratproxy」を組み合わせてのテスト方法を紹介します。

どちらも良く使われるツールなので、既に取りれている方もいるとは思いますが、両ツールを組み合わせることでより効率的にテストを行えるのではないでしょうか。

さっそくテスト方法について書いていきたいと思います。

1.ratproxyをインストール

以前、本ブログでも紹介されていましたが、Googleのセキュリティツール「ratproxy」をインストールします。
※参考:フリーで使えるセキュリティスキャナ・ツールまとめ

ダウンロードはこちらのページから行えます。

2.Selenium IDEをインストール

FireFoxのアドオンである、Webアプリケーションテストの自動化ツール「Selenium IDE」をインストールします。

Selenium IDEは、こちらのページから取得できます。

3.テスト用のデータ作成

Selenium IDEを使用してテスト用のデータを作っていきます。

Selenium IDEを起動します。起動した時点で記録状態となっているので、テストシナリオに沿って操作します。


一通りテストが完了したら、停止ボタンをクリックします。


テストケースを保存します。


4.ratproxyのプロキシ設定、起動

ブラウザのプロキシをratproxy用に設定します。


ratproxyを起動します。



./ratproxy -w hoge.log -r -lfscmXC  &


5.Selenium IDE実行

Selenium IDEを起動し、3.で作成したテストーデータを読み込み開始ボタンをクリックします。



6.ratproxyの結果確認

Selenium IDE実行後、ratproxyでの結果をHTML化し、ブラウザで表示します。



./ratproxy-report.sh hoge.log > hoge.html




使用方法はすごく基本的なところばかりですが、自動テストしながらセキュリティチェックまで行えてとても便利だと思います。
ぜひ、お試しください。

Webシステムのテスト

最近、受注案件や自社のWebシステムをテストする機会が増えてきたのですが、今回はテストの基本的な部分について簡単にまとめたいと思います。

※テストの重要性についてPHPプロ!で連載されている「TOM先生のテスト講座」も併せてご覧ください。

まず、テストを大きく以下の3つに分類し、それぞれについてまとめていきたいと思います。

○ 機能テスト
○ 非機能テスト
○ セキュリティテスト

■ 機能テスト
機能テストは、機能要求が満たされていて仕様通りに実装されているかを検証するためのテストです。

機能テストを行っていく上で様々なテスト方法がありますが、今回は以下のテスト方法が取り上げたいと思います。

・同値分割法:
 正当な値と不当な値の両方の代表値を入力してテストを行う方法。(例. 20以下までの入力欄で15と25を入力)
・境界値分析:
 正当な値と不当な値に分け、その境界となる値を入力してテストを行う方法。(例. 20以下までの入力欄で20と21を入力)
デシジョンテーブルテスト:
 入力と出力の関係をマトリクス形式で表して行うテスト方法。
・状態遷移テスト:
 仕様通りに状態間の遷移が網羅されているかを確認するためのテスト。
ユースケーステスト:
 成功のシナリオ(ユースケース)を作成して、それに沿ってテストを行っていく方法。

■ 非機能テスト
信頼性や安全性のためのテストです。
主に以下のように区分されます。

・性能テスト:
 データの増加により性能の劣化やレスポンスの低下などがないかをテスト。
・負荷テスト:
 システムに対して大量アクセスを発生させ、そのアクセスに耐えられるかをテスト。
・ストレステスト:
 高い負荷や想定を超えたアクセスを限界状態まで与え、データの破壊やシステムがダウンしないかをテスト。
・信頼性テスト:
 常に稼働するようなシステムにおいて、一定の時間、システムの機能を使用しダウンしないかをテスト。
・保守性テスト:
 機能の追加・修正やシステムの移行などで変更が加わる場合に、動作に問題がないかをテスト。
ユーザビリティテスト:
 ユーザインターフェースに問題(使いやすさやリンク切れ、ブラウザに依存する問題など)がないかをテスト。

■ セキュリティテスト
情報漏洩や不正アクセスなどを防ぐため、脆弱性がないかを検証するためのテストです。
※セキュリティをチェックするためのツールについては、「フリーで使えるセキュリティスキャナ・ツールまとめ」もご覧ください。

今回はすごく簡単にまとめましたが、次回機会があればもう少し詳しく書いていきたいと思います。