Protractor,Cucumberjs,ChaiによるAngularアプリケーションのテスト

#**はじめに**
[株式会社サイシード](http://sciseed.jp)のインターンシップにて利用しているテストツールについてまとめました。
Protractor,Cucumberjs,ChaiによるAngularjsアプリケーションのテストについて紹介します。

・Protractor（ http://www.protractortest.org/ ）：
アプリケーションのテストを行う場合はProtractorを使いましょう。
ビューの再レンダリングが完了する前にテスト処理が実行されてしまうといった事態を回避してくれます。

・Cucumberjs（ https://github.com/cucumber/cucumber-js/ ）：
自然言語に近い形で誰にでもわかりやすいテストコードを書くことができます。
featureとstepDefinitionがあり、featureにはテストのステップを書き、stepDefinitionにはそれぞれのステップに対する実際のテスト処理を定義します。

・Chai（ http://chaijs.com/ ）：
アサーション（テストの成功と失敗を判定する）のために利用します。

#**導入環境**
・macOS Sierra（10.12.6）
・Java（1.8.0_151）
・Node.js（v6.11.1）
・npm（3.10.10）

##**準備**
インストール手順など利用前の準備について説明します。
実際の使用方法についてのみ知りたい場合は「使用方法について」からお読みください。
##**1.　インストール**
Protractor、Cucumberjs、Chaiをインストールします。
また、Javaがインストールされている必要があります。

###**1.1　Protractorのインストール**

```text:ターミナル
$ npm install --save-dev  protractor
$ webdriver-manager update
$ webdriver-manager start
```
###**1.2　Cucumberjsのインストール**

```text:ターミナル
$ npm install --save-dev cucumber
```
###**1.3　ProtractorでCucumberjsを利用できるようにする**

```text:ターミナル
$ npm install --save-dev protractor-cucumber-framework
```
###**1.4　Chaiのインストール**

```text:ターミナル
$ npm install --save-dev chai chai-as-promised
```
###**1.5　coのインストール（async/awaitを使えるのであれば不要）**
ProtractorのほとんどのAPIがPromiseを返すので、Promiseを扱いやすくするためにcoをインストールする。
Nodejsのバージョンが7.6.0以上の場合は、async/awaitがデフォルトで使えるのでそちらを利用すると良いと思います。

```text:ターミナル
$ npm install --save co
```

##**2.　設定ファイルの編集**
Protractorの設定ファイルを以下のように編集します。
※ Cucumberjsのタグについて
タグは、「@タグ名」の形で記入する。
「~」を「@」の前に置くとそのタグ以外を指定したことになる。

```javascript:conf.js
exports.config = {

    // seleniumの待ち受けURLを指定
    seleniumAddress: 'http://localhost:4444/wd/hub',

    framework: 'custom', 

    // テストさせるブラウザを指定
    multiCapabilities: [
        { browserName: 'chrome' }
    ],

    frameworkPath: require.resolve('protractor-cucumber-framework'),

    // 実行するテストファイルを選択する
    specs: [
        'features/versionUpTest.feature'
    ],

　　// cucumberjsの設定
    cucumberOpts: {
　　　　　// 利用するstepDefinitionを指定する。
        require: [
            'features/step_definitions/stepDefinitions.js'
        ],
　　　　// 実行するfeatureまたはscenarioのタグを指定する。
        tags: '~@skip',
        strict: true,
        format: ['progress'],
        profile: false,
        'no-source': true,
    },
    onPrepare: function () {
        browser.manage().window().maximize(); // maximize the browser before executing the feature files
    }
};
```

##**使用方法について**
テストコードの書き方の例からテストコードの実行方法、Protractor、Cucumberjs、Chai、それぞれのよく利用するAPIなどを説明します。

##**1.　書き方の例**
Cucumberjsでは、featureと呼ばれるファイルに自然言語に近い形のテストコードを書き、一ステップごとのテスト内容の定義をStepDefinition呼ばれる別のファイルに書きます。
Protractor及びChaiはStepDefinitionで利用します。

###**1.1　Feature**

```cucumber:text:test.feature（例）

# language: ja

フィーチャ: テスト

    シナリオ: ログイン
        前提 "/"へページ遷移する
        かつ "input[type='email']"に"emailを入力する
        かつ "input[type='password']"に"password"を入力する
        かつ ".login-button"をクリックする
        ならば "/responselist"になる

    @skip
    シナリオ: メッセージに返信する
        前提 新着メッセージがある
        もし "/responselist"へページ遷移する
        もし ".response__textarea"に"メッセージ"を入力する
        かつ ".response__button"をクリックする
        ならば メッセージが送信される
```

###**1.2　StepDefinition**

```javascript:StepDefinitions.js（例）
//'use strict';
const co = require('co');
const EC = protractor.ExpectedConditions;

const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');

chai.use(chaiAsPromised);
const expect = chai.expect;

defineSupportCode(function({Given, Then, When, setDefaultTimeout}) {
  setDefaultTimeout(30 * 1000);

  Given(/^"(.*)"へページ遷移する$/, function(url, done){
    console.log(`login finished ${url}`);
    co(function*() {
        browser.ignoreSynchronization = true;
        browser.get(`${baseUrl}${url}`);
        browser.ignoreSynchronization = false;
        done();
    });
  });

  When(/^"(.*)"に"(.*)"を入力する$/, function(cssName, text, done){
    co(function*() {
      yield element(by.css(cssName)).sendKeys(text);
      done();
    });
  });

  When(/^"(.*)"をクリックする$/, function(cssName, done){
    co(function*() {
      yield element(by.css(cssName)).click();
      done();
    });
  });

  Then(/^"(.*)"が存在する$/, function(cssName, done){
    co(function*() {
      expect(element.all(by.css(cssName))).to.exist;
      done();
    });
  });

});
```

##**2.　テストの実行方法**
一番はじめに以下を実行します。

```text:ターミナル
$ webdriver-manager update
```
上のコマンドを一度実行してからは、それ以降は以下の２つのコマンドを別々のターミナルで実行すればテストを実行することができる。
webdriverのサーバーを立ち上げる。

```text:ターミナル
$ webdriver-manager start
```

実行したいテストの設定ファイルを指定して実行する。

```text:ターミナル
$ protractor conf.js
```

##**2.　Protracorの使い方**
よく利用するAPIの説明と私がつまづいた部分についての補足をします。

###**2.1　よく利用するAPI**
ここで説明しているAPI以外も知りたい場合は次のURLをご覧ください。
http://www.protractortest.org/#/api

####**[ページに対する処理]**

#####**(1)　Angular以外の処理を行う場合はProtractorの同期機能を切っておく**

```js
browser.ignoreSynchronization = true;
```

#####**(2)　”path”のページに移動する**

```js
browser.get(path);
```

　
####**[要素を指定する]**

#####**(3)　”cssName”で指定した要素を取得する**

```javascript:
element(by.css(cssName));
```

#####**(4)　”cssName”で指定した要素のうち”text”を含むものを取得する**

```javascript:
element(by.cssContainingText(cssName, text));
```

　
####**[指定した要素に対して処理を行う]**

#####**(5)　指定した要素に"text"を入力する**

```js
sendKeys(text)

//返り値はPromise。
```

```javascript:例
element(by.css(cssName)).sendKeys(text);
```

#####**(6)　指定した要素をクリックする**

```js
click()

//返り値はPromise。
```

```javascript:例
element(by.css(cssName)).click();
```

#####**(7)　指定した要素の数を数える**

```js
count()

//返り値はPromise。
```

```javascript:例
element.all(by.css(cssName)).count();
```


#####**(8)　指定した要素のテキストを取得する**


```js
getText()

//返り値はPromise。
```

```javascript:例
element(by.css(cssName)).getText();
```

#####**(9)　指定した要素の"attributeName"属性を取得する**

```js
getAttribute(attributeName)

//返り値はPromise。
```

```javascript:例
element(by.css(cssName)).getAttribute(attributeName);
```

#####**(10)　指定した要素が存在する場合はtrueを返す**

```js
isPresent()

//返り値はPromise。
```

```javascript:例
element(by.css(cssName)).isPresent();
```

　
####**[要素やページなどが変化したかどうかを判定する]**

#####**(11)　”path”のページになるまで待つ**

```js
protractor.ExpectedConditions.urlIs(path)

//返り値はPromise。
```

```javascript:例
browser.wait(protractor.ExpectedConditions.urlIs(path), waitTimeDefault);
```

###**2.2　 補足**

#####**(1)　待ってから処理を行いたい時は、thenのブロック内に書く**
ProtractorのAPIの返り値はPromiseであることが多いため。
coを利用して以下のように書くとわかりやすい。

```javascript:StepDefinitions.js
co(function*() {
      yield browser.wait(EC.urlIs(path), waitTimeDefault);
});
```

#####**(2)　値を取得する処理の返り値がPromiseのとき、具体的な値を取得したい時は、thenで取得する**

```javascript:StepDefinitions.js
co(function*() {
      const count = yield element.all(by.css(cssName)).count();
});
```

#####**(3)　Protractorでは各WebDriverの処理の前でwaitForAngular()が呼ばれる**
waitForAngular()が自動的に呼ばれることで、基本的に手動で待ち処理を書く必要がなくなる。
Angularがレンダリングを終えるまで待ち、未処理のhttpまたはhttpsリクエストがないことを確認する。

#####**(4)　要素を指定するときの省略**
`element.all(by.css(cssName)).first();`
`element.all(by.css(cssName)).get(0);`
は全て同じで、 以下のように省略することもできる。
`element(by.css(cssName));`

#####**(5)　要素を指定するときのメソッドチェーン**
指定した要素の下にある要素を順々に指定していくことができる。
`element(by.css(cssName)).element(by.css(cssName))`
ある要素の下にあるものを全て指定したい時は以下のようにする。
`(一つに絞った要素).all(by.css(cssName));`

```javascript:例
element.all(by.css(cssName)).get(0).all(by.css(cssName))
```

##**3.　Cucumberjsの使い方**
Featureのテスト内容を定義するStep DefinitionsとFeatureあるいはScenarioの前後に処理を挟むことができるHooksについて説明します。
詳しく知りたい場合は以下をご覧ください。
https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/api_reference.md
また、私がつまづいた部分の補足をします。

###**3.1　 Step Definitions**
defineStep()でfeatureのステップを定義する。
エイリアスとして、Given, When, Thenがある。
さらに、featureでは、And、Butも利用することができる。
第一引数には文字列または正規表現を渡す。
第二引数のfunctionには正規表現で任意の文字列を指定した部分に該当するものが引数として渡される。

```javascript:StepDefinitions.js（例）
When(/^"(.*)"の内"(.*)"が含まれるものをクリックする$/, function(cssName, text, done){
    co(function*() {
      yield element(by.cssContainingText(cssName, text)).click();
      done();
    });
});
```

###**3.2　 Hooks**
各Senarioの前後、最初のSenarioが始まる前と最後のSenarioが終わった後で実行したい処理を記述することができる。
どのタグを持ったFeatureまたはSenarioに対して行うか指定することもできる。

```javascript:StepDefinitions.js（例）
var {After, Before} = require('cucumber');

Before(function () {
  // This hook will be executed before all scenarios
});

Before({tags: "@foo"}, function () {
  // This hook will be executed before scenarios tagged with @foo
});
```

#####**(1)　Before**
各シナリオの前に実行したい処理を記述する。

#####**(2)　After**
各シナリオの後に実行したい処理を記述する。

#####**(3)　BeforeAll**
最初のシナリオの前に実行したい処理を記述する。

#####**(4)　AfterAll**
最後のシナリオの後に実行したい処理を記述する。

###**3.3　 補足**
#####**(1)　Given、When、ThenはdefineStepのエイリアス**
Given、When、Thenは全て同じメソッドのエイリアスなので、featureで利用する際はどれで使っても良いです。
ただし、わかりやすくするためにもできる限り用途ごとに使い分けると良いと思います。

#####**(2)　setDefaultTimeout()でタイムアウト時間を設定する**
”milliseconds”でテストのタイムアウト時間を設定することができます。

```js
setDefaultTimeout(milliseconds);
```

#####**(3)　stepDefinitionのステップに対する処理では最後にdone()を呼ぶ**

```javascript:StepDefinitions.js（例）
When(/^"(.*)"から"(.*)"を選択する$/, function(cssName, value, done){
    co(function*() {
      yield element(by.css(cssName)).element(by.cssContainingText('option', value)).click();
      done()
    });
});
```

#####**(4)　日本語でテストを書く**
featureファイルの最初に以下のコードを書く。

```cucumber
# language: ja
```

以下のコマンドで英語と日本語の対応を確認することができる。

```text:Linux
$ cucumber --i18n ja
```

##**4.　Chaiの使い方**
よく利用するAPIと私がつまづいた部分の補足をします。

###**4.1　よく利用するAPI**
ここで説明しているAPI以外も知りたい場合は次のURLをご覧ください。
http://chaijs.com/api/bdd/

以下で、cssNameは任意のCSSセレクタ、textは任意のテキスト。

#####**(1)　要素が表示されたか（存在しているか）を判定する**

```js
exist
```

```javascript:例
expect(element.all(by.css(cssName))).to.exist;
```

#####**(2)　ある文字が文字列に含まれるか、ある要素が配列に含まれるかを判定する**

```js
include(text)
```

```js:例
expect(element(by.css(cssName)).getText()).to.include(text);
```

#####**(3)　trueになったかどうかを判定する**

```js
true
```

```javascript:例
expect(element(by.css(cssName)).isPresent())to.be.true;
```

**(4)　型も含め等しいかどうかを判定する**

```js
equal(text)
```

```javascript:例
expect(element(by.css(cssName)).getText()).to.equal(text);
```

###**4.2　補足**
#####**(1)　Chai as promisedについて**
Chai as promisedでPromiseがresolveした後の値に対して判定を行う。
Chai as promisedで追加されるeventuallyメソッドを利用する。

```javascript:例
expect(element(by.css(cssName)).getText()).eventually.to.include(text);
// cssNameはCSSセレクタ、textは任意のテキスト
```

#####**(2)　toやbeなどはわかりやすさのために使うのみで、実際的な処理は行わない**

#**参照**
[1]　[AngularJS 向けの E2E テストツール「Protractor」で要素を特定するアレコレ](http://neos21.hatenablog.com/entry/2017/07/03/080000)
[2]　[Cucumber のフィーチャの文法 - Gherkin](http://blog.eiel.info/blog/2013/02/12/gherkin/)