ProtractorとAngularJSではじめてのE2Eテスト

この記事は[前回](/suetake/items/98defc18a97f7b4561fe)からの続きとなります。

---

AngularJSのE2Eテストといえば、Protractorですね。[公式](http://www.protractortest.org/)ページにもある`for AngularJS`の記述がなんとも心強いです。
ProtractorはAngularJSのために開発されているだけあり、テスト実行時に何も考えずとも__AngularJSと同期してくれる__という最強に素晴らしい機能を備えています。

## 何を同期してくれるのか

AngularJSはモデルの変更を検知するために`Scope Life Cycle`という機構を備えています。この機構によりモデルの変更は即座に検知され、同時にビューを最新の状態に保ちます。Protractorはこのライフサイクル中に実行される`$digest`ループの完了を検知（同期）してくれるので、ビューの再レンダリングが完了する前にテスト処理が実行されてしまうといった事態を確実に回避してくれます。
また、`$timeout`、`$resource`, `$http`を用いた非同期処理（通信）もしっかりと待機してくれます。

## セットアップ

では早速環境構築していきましょう。Protractorのインストールには`npm`を利用します。
下記コマンドでグローバル領域にProtractorをインストールします。

```bash:command
npm install -g protractor
```

上記コマンドにより`protractor`, `webdriver-manager`の2つのコマンドラインツールがインストールされます。`protractor --version`でバージョンを確認することが出来ます。
`webdriver-manager`は動作中のSeleniumサーバーのインスタンスを取得するためのヘルパーツールです。

次のコマンドを実行し、動作に必要なバイナリをダウンロードします。

```bash:command
webdriver-manager update
```

完了後、次のコマンドでSeleniumサーバーを起動させます。

```bash:command
webdriver-manager start
```

- Protractorはテスト実行時ローカルのブラウザを操作するため、Seleniumサーバーへリクエストを送ります。
- [`http://localhost:4444/wd/hub`](http://localhost:4444/wd/hub)にアクセスするとSeleniumサーバーの状態を確認することが出来ます。

以上でセットアップは完了です。

## テストを書く

Protractorを用いたE2Eテストを実行するためには最低2つのファイルを作成する必要があります。

- 利用するFrameWorkやSeleniumサーバーのアドレスを指定しておくための設定ファイル
- 実際のテストコードを記述したファイル

の2つです。


### conf.js

まずは設定ファイルから記述してみます。下記のように記述します。

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

    // Testing FrameWorkにjasmineを利用
    framework: 'jasmine',

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

    // 動作させるspecファイルを指定
    specs: ['todoListSpec.js'],

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

### todoListSpec.js

次に実際に走らせるテストコードを記述していきます。

```js:todoListSpec.js
/**
 * todoListコンポーネントに関連するE2Eテストグループ
 */
describe('todoListコンポーネントのE2Eテスト', function() {

    // タスク入力用のInput要素への参照を取得
    var taskInput = element(by.model('$ctrl.data.text'));

    // 追加ボタンへの参照を取得
    var addButton = element(by.buttonText('追加'));

    // タスク表示用リピート要素への参照を取得
    var tasks = element.all(by.repeater('item in $ctrl.items'));

    beforeEach(function() {

        // todoComponentを使用しているページへアクセス(今回で言うとindex.htmlが表示されるURL)
        browser.get('http://todo.local.lcl');
    });

    it('初期タスクとして3個のタスクがレンダリングされているか', function() {

        // `by.repeater()`で指定した要素が3個レンダリングされているか
        expect(tasks.count()).toEqual(3);
    });

    it('タスク追加、削除の流れが正常に行えるか', function() {

        // タスク入力用Inputに指定の文字列を入力
        taskInput.sendKeys('追加タスク');

        // 追加ボタンをクリック
        addButton.click();

        // 追加後、リピート要素が4個になっているか
        expect(tasks.count()).toEqual(4);

        // 追加したタスクのテキストがInputへ入力したものと一致しているか
        expect(tasks.last().$('span').getText()).toEqual('追加タスク');

        // タスクを古いものから順に3個削除する
        tasks.last().$('button').click();
        tasks.last().$('button').click();
        tasks.last().$('button').click();

        // リピート要素が1個になっているか
        expect(tasks.count()).toEqual(1);
    });

});
```

Jasmineの使い方は前回と変わりません。`describe()`でテストグループを作成し、`it()`で個々のテスト内容を記述していきます。

Protractor特有のオブジェクトとして`element`, `by`, `browser`があります。
`element`はその名の通りエレメントへの参照を取得するための関数、
`by`はエレメントへの参照をどのように解決するかを定義するための機能を多数保有しており、`binding()`, `model()`, `buttonText()`, `repeater()`といった関数を提供しています。`binding()`は`ng-bind`、もしくは`{{}}`でバインドしている変数名をセレクタとして渡します。`model()`へは`ng-model=""`で指定している変数名、`buttonText()`へはボタン要素のテキスト部分、`repeater()`へは`ng-repeat=""`に渡している文字列を指定したり出来ます。
`by`は他にも便利な関数がたくさんありますが、詳細は[公式リファレンス](http://www.protractortest.org/#/api)を参照するのが良いかと思います。

`browser.get()`で引数に渡したURLにアクセスしてくれます。[前回作成したTODOアプリ](/suetake/items/f61b4413eefeb60ef7fb)のテストを行いたいので、そのアプリが閲覧出来るURLを指定しています。

## 実行

では実行してみます。
実行する際は下記のように`conf.js`を指定して起動します。

```bash:command
protractor conf.js
```

すると指定したブラウザが立ち上がり一瞬でテストが実行され、完了後ブラウザが終了します。

![d3db2c05c14233d200b20731f198a421-compressor.gif](https://qiita-image-store.s3.amazonaws.com/0/112725/30238811-df28-3ab8-fffe-7a7629e0ab79.gif)

今回はChromeとFireFoxを指定したので、2つのブラウザが立ち上がりテストが実行されました。サックサクですね。

結果はコマンドを実行したコンソール上に下記のように表示されます。

```
[launcher] Running 2 instances of WebDriver
..
------------------------------------
[chrome #11] PID: 448
[chrome #11] Specs: /var/www/html/todo/e2eTest/todoListSpec.js
[chrome #11] 
[chrome #11] Using the selenium server at http://localhost:4444/wd/hub
[chrome #11] Started
[chrome #11] ..
[chrome #11] 
[chrome #11] 
[chrome #11] 2 specs, 0 failures
[chrome #11] Finished in 2.283 seconds

[launcher] 1 instance(s) of WebDriver still running
..
------------------------------------
[firefox #01] PID: 443
[firefox #01] Specs: /var/www/html/todo/e2eTest/todoListSpec.js
[firefox #01] 
[firefox #01] Using the selenium server at http://localhost:4444/wd/hub
[firefox #01] Started
[firefox #01] ..
[firefox #01] 
[firefox #01] 
[firefox #01] 2 specs, 0 failures
[firefox #01] Finished in 2.212 seconds

[launcher] 0 instance(s) of WebDriver still running
[launcher] chrome #11 passed
[launcher] firefox #01 passed
```

どちらのブラウザも`passed`が表示されています。成功です。

## おわりに

Protractorを用いたE2Eテストの雰囲気は伝わったでしょうか。E2Eテストは書いていて本当に楽しいですね。テストを動作させた時の「シュババババババッ」感がたまりません（哲学）。

## P.S.

__テスト書こう。絶対。__