generator-angular環境に手を加えて、protractorでE2Eテストを動かしてみる。
e2eテストってカッコ良いですよね
AngularJSの公式チュートリアルにはE2Eテストのチュートリアルも含まれています。
これを動かすと、ブラウザが立ち上がって、文字入力や画面遷移が自動的に実行され、テストされていきます。
個人的にはユニットテストだけでテストを担保するのに若干の不安がありましたので、ここまで外のレイヤーでのテストが継続的に行われていると安心できるかなぁと思っています。(もっともテストを書く手間が大変そうではありますが・・・)
是非ともこのテストも標準で行っていきたいのですが、generator-angular
では標準で動かす準備がないようです。
ng-scenario
を使ったやりかけの部分はあるようですが、AngularJSの公式では「ng-scenario
じゃなくてprotractor
使いなよ」って言われていますので、そちらを使えるようにいろいろ調べて行きたいと思います。
protractorを動かすためのサンプルページを作る
今回はprotractor
を動かすだけなので簡単なページです。
yo angular
でできたアプリを簡単に改造しました。
なおデータの取得はソースベタになっていますのであしからず。
<div class="container" ng-controller="MainCtrl">
<div class="row">
<div class="col-xs-3">
<form class="well" role="form">
<div class="form-group">
<label for="searchText">Test Search</label>
<input id="searchText" type="text" class="form-control" ng-model="query.$" placeholder="input search text">
</div>
<div class="form-group">
<label for="sortKey">Sort Key</label>
<select id="sortKey" class="form-control" ng-model="sort.key">
<option value="id">Default</option>
<option value="title">Title</option>
</select>
</div>
<div class="form-group">
<label for="sortOrder">Sort Revers</label>
<input id="sortOrder" type="checkbox" ng-model="sort.revers"></input>
</div>
</form>
</div>
<div class="col-xs-9" class="items-panel">
<div class="panel panel-default" ng-repeat="e in items | filter:query | orderBy:sort.key:sort.revers">
<div class="panel-heading">
<div class="item-title" ng-bind="e.title"></div>
</div>
<div class="panel-body">
<div class="item-description" ng-bind="e.description"></div>
</div>
</div>
</div>
</div>
</div>
以下はJSのコード。
'use strict';
angular.module('testApp')
.controller('MainCtrl', function ($scope) {
$scope.items = [
{
id:1,
title:'HTML5',
description: '説明文は省略'
},
{
id:2,
title:'AngularJS',
description: '説明文は省略'
}
];
$scope.query = {
$: ''
};
$scope.sort = {
key:'id',
revers:false
};
});
とりあえず、サンプルで動かすテストコードです。
AngularJSのサンプルから拾ってきたものです。(画面の挙動とは一切関係ありませんね・・・)
describe('angularjs homepage', function() {
it('should greet the named user', function() {
browser.get('http://www.angularjs.org');
element(by.model('yourName')).sendKeys('Julie');
var greeting = element(by.binding('yourName'));
expect(greeting.getText()).toEqual('Hello Julie!');
});
});
protractorを動かす
Grunt
でprotcator
を動かすモジュールを探したら、grunt-protractor-runner
なるものがヒットしたので、そちらを使ってみます。
https://github.com/teerapap/grunt-protractor-runner
インストールはいつも通りコマンド一発。
$ npm install grunt-protractor-runner --save-dev
Gruntfile
にタスクを追記します。
protractor: {
options: {
configFile: "node_modules/protractor/referenceConf.js",
keepAlive: true,
noColor: false,
args: {
}
},
sample: {
options: {
configFile: "e2e.conf.js",
args: {
browser: 'chrome'
}
}
},
},
// 中略
grunt.registerTask('e2e', function(){
grunt.task.run([
'clean:server',
'concurrent:test',
'autoprefixer',
'connect:test',
'protractor'
]);
});
protractorの設定ファイルは以下のとおりです。
exports.config = {
allScriptsTimeout: 11000,
specs: [
'test/e2e/{,*/}*.js'
],
capabilities: {
// 'browserName': 'chrome'
// 'browserName': 'firefox'
// 'browserName': 'safari'
},
chromeOnly: false,
baseUrl: 'http://localhost:9001/',
framework: 'jasmine',
jasmineNodeOpts: {
defaultTimeoutInterval: 30000
}
};
※注意!
chromeOnly
をfalse
に設定している場合、WebDriverを使うことになりますが、その場合はJDKのインストールが必須になります。事前に入れておいてください。
あと、立ち上げるブラウザの種類ですが、Gruntfile
で指定する場合はbrowser
でe2e.conf.jsで指定する場合はbrowserName
になります。(キーが違ってハマった・・・)
指定箇所を明示するためにあえてコメントで残してあります。
さて、準備万端、意気揚々とコマンド実行です。
$ grunt e2e
が、ダメ!動かない・・・
どうやらprotractor
に同梱のwebdriver-manager
を走らせる必要がありそうです。いろいろ調べたのですが、どうもgrunt-protractor-runner
は自身のnode_modules
ディレクトリ配下のprotractor
を使っているらしく、そちらのwebdriver-manager
を実行してねって書いてありました。で、タスクは?うん、ないですね・・・えぇ〜!?
ちなみに変更履歴に以下のような記述がありました。
Temporarily remove automatically download/update webdriver-manager because it fails in some environment such as Windows (#41)
諦めんなよ!
しかたがないので、手実行、と言いたいところですが、これだと環境構築でめんどくさいことになりそうなので別の方法を探してみますが・・・見つからないです・・・
※途中grunt-protractor-webdriver
というプラグインが見つかったのですが、これは違った・・・
幸い、protractor
が個別にインストールされている場合はそちらを参照してくれるような作りになっているぽいので、何とかそちらで管理をしてみようと思います。
※グローバルインストールも考えたのですが、環境が個別にまとまっている感じが良かったので・・・
少々強引ではありますがgrunt-shell
を使ってテスト前にwebdriver-manager
のupdateを走らせることにします。
まずは必要なプラグインをインストール。
$ npm install protractor --save-dev
$ npm install grunt-shell --save-dev
続いてコマンドを登録します。
shell: {
webdriver_update: {
command: 'node_modules/.bin/webdriver-manager update'
}
}
最後にタスクを修正します。
grunt.registerTask('e2e', function(){
grunt.task.run([
'clean:server',
'concurrent:test',
'autoprefixer',
'connect:test',
'shell:webdriver_update',
'protractor'
]);
});
いざ実行。
$ grunt e2e
できた!けどイケてねぇ〜!
とりあえず作者の今後の活動に期待しときます・・・
※ここまで書いてnpm
のpostinstallもアリかなぁとちょっと思いました・・・
実際のテストを書いてみる。
実際にサンプルに対してE2Eテストを書いてみました。
'use strict';
describe('sample e2e tests.', function() {
var searchText,sortKey,sortRevers,itemsPanel;
beforeEach(function() {
browser.get('/index.html');
searchText = $('#searchText');
sortKey = $('#sortKey');
sortRevers = $('#sortOrder');
itemsPanel = $('.items-panel');
});
it('default search setting.', function(){
expect(searchText.getText()).toBe('');
expect(sortKey.getAttribute('value')).toBe('id');
expect(sortRevers.isSelected()).toBeFalsy();
itemsPanel.$$('.item-title').then(function(items){
expect(items.length).toBe(2);
expect(items[0].getText()).toBe('HTML5');
expect(items[1].getText()).toBe('AngularJS');
});
});
it('search "ang" result.', function(){
searchText.sendKeys('ang');
itemsPanel.$$('.item-title').then(function(items){
expect(items.length).toBe(1);
expect(items[0].getText()).toBe('AngularJS');
});
});
it('search sort by title.', function(){
$('#sortKey > option[value=title]').click();
itemsPanel.$$('.item-title').then(function(items){
expect(items.length).toBe(2);
expect(items[0].getText()).toBe('AngularJS');
expect(items[1].getText()).toBe('HTML5');
});
});
it('search sort desc.', function(){
sortRevers.click();
itemsPanel.$$('.item-title').then(function(items){
expect(items.length).toBe(2);
expect(items[0].getText()).toBe('AngularJS');
expect(items[1].getText()).toBe('HTML5');
});
});
});
今回はあえてAngularJSに特化しない形でテストケースを書いています。
具体的には、DOMの検証にはCSSセレクターのみを利用し、その要素に対してイベントを発行している、といった具合です。
基本的な書き方は恐らくJasmineに準拠するのですが、protractor用に幾つか便利なメソッドが提供されています。詳しい使い方は、公式のマニュアルが詳しいのでそちらを参照にすると良いです。
https://github.com/angular/protractor/blob/master/docs/api.md
以下、トピックを抜粋して紹介します。
ElementFinder.prototype.$
jQuery
使えるのかな?と思いきや、実は全く違います。ElementFinder
というオブジェクトをCSSセレクターで返すエイリアスの関数です。
ElementFinder
は単一のDOMオブジェクトというイメージです。複数のオブジェクトを同一視することはなく、複数オブジェクトを取り扱う場合はElementArrayFinder
というオブジェクトを使います。(ちょっと不便な気がしますね・・・今後改善されるのでしょうか?)
余程のことがなければjQuery
ライクに書けるとは思いますが、より厳密に書いてあげたほうが良さそうですね。
ElementFinder.prototype.$$
こちらはElementArrayFinder
を返します。上述の通り、こちらは複数のオブジェクトというイメージですね。
複数のDOMが帰ってきますので、実際の値を取得するget
やらcount
やらが用意されています。get
で帰ってくるのはElementFinder
になります。
ここで非常に注意したいのが、ElementFinder
やElementArrayFinder
から値を取得するメソッドとして提供されているgetText
やcount
といったメソッドの戻り値はPromise
になっているということです。
Promise
の詳細については割愛しますが、ざっくり言うと、「非同期通信の結果を取得するためのエンドポイント」ということです。(Java
でいうところのFutureみたいなものでしょうか)
Promise
を通じて取得される値は非同期で順序が保証されていませんので、今回のテストのようにthen
などを使って、コールバック経由でないと実際の値は取得できません。したがって、以下の様な書き方はできません。
if ($('#hogehoge').getText() == 'inputValue') {
// logic
}
代わりに以下のように書きます。
$('#hogehoge').getText().then(function(text){
if (text == 'inputValue') {
// logic
}
});
もうこうなってくるとわけわからなくなります。
コールバックの呼び出しより先に後続ロジックが動いてしまいますので、処理をシリアルに実行しようとするとthen
で返ってくるPromise
を更にチェインして処理を書いて・・・なんて事になります。実際に、セレクトボックスの値を取得しようとループで回して選択値を取得して検証、なんてテストを書こうとしてえらく大変でした。なので、DOMの取得は極力CSSセレクタで一発で取れるよう記述しないと話になりませんね・・・
「値の比較ができないってことはexpect
はちゃんと動くの?」と思ったのですが、こちらは正常に動作しているようです。中身までは確認していませんが、expect
による検証は内部的にPromise
を解釈して値検証をしていると思われます。expect
自体はそれぞれが独立した検証であり、検証順序の厳密性は問われないからでしょう。なるほど、頭良いですね。
恐らく、WebDriverとの値のやり取りはプロセス間通信が発生するため、即時に値を取得してロジックに反映させることができないのではないかと推測しています。まぁこればかりは仕方ないですね・・・
webdriver.WebElement.prototype.click
DOM要素をクリックする処理です。「テストコードではElementFinder
のclick
を呼んでいるけど大丈夫?」と思ったのですが、こちらは大丈夫なようです。
The ElementFinder can be treated as a WebElement for most purposes, in particular, you may perform actions (i.e. click, getText) on them as you would a WebElement.
webdriver.WebElement.prototype.sendKeys
こちらはキー入力を行うものです。
基本的な使い方は上述のclick
と同じです。
まとめ
今回はあえてAngularJSに依存しないE2Eテストを書いてみましたが、AngularJSを使う場合はby.model
などAnuglarJSの変数に直接アクセスできるようなコードも書けるみたいですので、よりテストはし易いかなぁと感じました。
しかしながら、今回のようにCSSセレクターを利用したDOMの取得と、クリックとキー入力というイベントの発火を行うだけであれば、非常に汎用的ですし、AngularJSを使っていないアプリに対してもテストが書けます。protractor
はこの辺のサポート実装を行っていますのでテストのコードの記述量を抑える意味でも導入するメリットは大いにあります。
また、今回は試していませんが、distで本番用にコンパイルしたコードをベースにしたテストなどもできそうです。変なミスをなくす上では重要なアプローチですよね。
テスト自体が自動化できたということでJenkins
などのCIツールと組み合わせたりすると、テストが捗りそうですので、機会があればその辺も調査していきたいと思います。
ただ、実際のアプリとなると、連携させるAPIの準備であったり、APIが返すデータをDBなどに初期投入するなど、まだまだやることは多そうです。今後の課題ですね。
とりあえず、開発するにあたって必要な環境構築はひと通りできたかなぁと思います。これでやっとAngularJSを使ったアプリ開発に入れますね。(長かった・・・)
今後は、実際のAnuglarJSを触りながら、気になったトピックなどを書いていきたいですね。(まぁあくまで備忘録なのであしからず)