PhantomJSラッパーのNightmareでUIをテストする

# はじめに
[PhantomJS](http://phantomjs.org/)のラッパーである[Nightmare](http://www.nightmarejs.org/)を使って、画面を操作するテストを行ってみます。
より本格的な検証になるよう、[TodoMVC](http://todomvc.com/)のサンプルをテスト対象として使用します。

ちなみに、Phantom.js関連のライブラリとしては[CasperJS](http://casperjs.org/)がありますが、Nightmare本家トップページのサンプルを見るに、CasperJSよりもさらにシンプルに書けそうです。
# インストール

```
> npm install nightmare
```

※Phantom.jsを事前にインストールしておいてください。
## インストール時にエラーが出た場合
筆者のWindows 7環境でインストールしたところ、下記のエラーが発生しました。

```zsh
C:\Users\**>npm install nightmare
|
> weak@0.3.3 install C:\Users\**\node_modules\nightmare\node_modules\phantom\node_modules\dnode\node_modules\weak

> node-gyp rebuild


C:\Users\**\node_modules\nightmare\node_modules\phantom\node_modules\dnode\node_modules\weak>node "C:\Program Fil
es\nodejs\node_modules\npm\bin\node-gyp-bin\\..\..\node_modules\node-gyp\bin\node-gyp.js" rebuild
Building the projects in this solution one at a time. To enable parallel build, please add the "/m" switch.
MSBUILD : error MSB3428: Visual C++ コンポーネント "VCBuild.exe" を読み込めませんでした。この問題を解決するには、次のいずれかを行ってください。 1) .NET F
ramework 2.0 SDK インストールする。 2) Microsoft Visua
l Studio 2005 をインストールする。 3) その他の場所にインストールされている場合、コンポーネントの場所をシステム パスに追加する。 [C:\Users\**\node_modules\nightmare\no
de_modules\phantom\node_modules\dnode\node_modules\weak\build\binding.sln]
gyp ERR! build error
gyp ERR! stack Error: `C:\Windows\Microsoft.NET\Framework\v4.0.30319\msbuild.exe` failed with exit code: 1
gyp ERR! stack     at ChildProcess.onExit (C:\Program Files\nodejs\node_modules\npm\node_modules\node-gyp\lib\build.js:267:23)
gyp ERR! stack     at ChildProcess.emit (events.js:98:17)
gyp ERR! stack     at Process.ChildProcess._handle.onexit (child_process.js:810:12)
gyp ERR! System Windows_NT 6.1.7601
gyp ERR! command "node" "C:\\Program Files\\nodejs\\node_modules\\npm\\node_modules\\node-gyp\\bin\\node-gyp.js" "rebuild"
gyp ERR! cwd C:\Users\**\node_modules\nightmare\node_modules\phantom\node_modules\dnode\node_modules\weak
gyp ERR! node -v v0.10.32
gyp ERR! node-gyp -v v1.0.1
gyp ERR! not ok
npm WARN optional dep failed, continuing weak@0.3.3
nightmare@1.3.2 node_modules\nightmare
├── debug@0.7.4
├── defaults@1.0.0
├── once@1.3.0
├── clone@0.1.18
└── phantom@0.6.6 (win-spawn@2.0.0, traverse@0.6.6, shoe@0.0.15, dnode@1.2.0)
```

エラーログを見るに、node-gypのビルドに失敗しているようでした。
[node-gypのGithubページ](https://github.com/TooTallNate/node-gyp)にアクセスしてみると、『[Installation](https://github.com/TooTallNate/node-gyp#installation)』に下記のように記載されていました。

> You will also need to install:
> 
>   * On Unix:
>     * `python` (`v2.7` recommended, `v3.x.x` is __*not*__ supported)
>     * `make`
>     * A proper C/C++ compiler toolchain, like GCC
>   * On Windows:
>     * [Python](http://www.python.org/getit/windows) ([`v2.7.3`](http://www.python.org/download/releases/2.7.3#download) recommended, `v3.x.x` is __*not*__ supported)
>     * Windows XP/Vista/7:
>       * Microsoft Visual Studio C++ 2010 ([Express](http://go.microsoft.com/?linkid=9709949) version works well)
>       * For 64-bit builds of node and native modules you will _**also**_ need the [Windows 7 64-bit SDK](http://www.microsoft.com/en-us/download/details.aspx?id=8279)
>         * If the install fails, try uninstalling any C++ 2010 x64&x86 Redistributable that you have installed first.
>       * If you get errors that the 64-bit compilers are not installed you may also need the [compiler update for the Windows SDK 7.1](http://www.microsoft.com/en-us/download/details.aspx?id=4422)
>     * Windows 7/8:
>       * Microsoft Visual Studio C++ 2012 for Windows Desktop ([Express](http://go.microsoft.com/?linkid=9816758) version works well)

上記を参考に、『Microsoft Visual Studio C++ 2012 for Windows Desktop』をインストール後、再度インストールしたところ、エラーログが出なくなりました。

# サンプル
今回、テスト対象は[TodoMVC](http://todomvc.com/)の[Backbone.js版Todoリスト](http://todomvc.com/examples/backbone/)を使用します。

Nightmareでのテストの書き方は、NightmareのGithubリポジトリにある[テストコード](https://github.com/segmentio/nightmare/tree/master/test)を参考にしています。

## インストール

```
> npm install --save-dev mocha
```

```
> npm install --save-dev should
```
## テストコード

```js:todoMvc_test.js
var Nightmare = require('nightmare');
var should = require('should');

var settings = {
  timeout: 20000,
  applicationUrl: 'http://todomvc.com/examples/backbone/'
};

describe('画面遷移', function () {
  this.timeout(settings.timeout);

  it('TODOリストが表示できること', function (done) {
    new Nightmare()
        .goto(settings.applicationUrl)
        .run(function () {
          done();
        });
  });
});

describe('基本動作確認', function () {
  this.timeout(settings.timeout);
  var nightMare;

  beforeEach(function (done) {
    nightMare = new Nightmare()
        .goto(settings.applicationUrl)
        .evaluate(function () {
          // 各テストは初期状態で実施するため、TODOリストデータをクリアする
          localStorage.clear();
        })
        .goto(settings.applicationUrl);
    done();
  });

  it('TODO追加ができること', function (done) {
    var todoText = 'テストToDoそのいち';

    nightMare
        .type('#new-todo', todoText)
        .evaluate(function () {
          var e = $.Event('keypress');
          e.which = 13;
          // エンターキー押下
          $('#new-todo').trigger(e);
          // 追加されたToDo文字列を返す
          return $('#todo-list li:last').find('label').text();
        }, function (addedTodoText) {
          addedTodoText.should.equal(todoText);
        })
        .run(done);
  });

  it('追加したTODOの数が表示されること', function (done) {
    var todoText = 'テストToDo';

    nightMare
        .type('#new-todo', todoText)
        .evaluate(function () {
          var e = $.Event('keypress');
          e.which = 13;
          // エンターキー押下
          $('#new-todo').trigger(e);
        })
        .evaluate(function () {
          return $('#todo-count').find('strong').text();
        }, function (todoCount) {
          todoCount.should.be.equal('1');
        })
        .run(done);
  });

  it('複数のTODO追加ができること', function (done) {
    var todoText1 = 'テストToDoそのいち';
    var todoText2 = 'テストToDoそのに';

    nightMare
        .type('#new-todo', todoText1)
        .evaluate(function () {
          var e = $.Event('keypress');
          e.which = 13;
          // エンターキー押下
          $('#new-todo').trigger(e);
          // 追加されたToDo文字列を返す
          return $('#todo-list li:last').find('label').text();
        }, function (addedTodoText) {
          addedTodoText.should.equal(todoText1);
        })
        .type('#new-todo', todoText2)
        .evaluate(function () {
          var e = $.Event('keypress');
          e.which = 13;
          // エンターキー押下
          $('#new-todo').trigger(e);
          // 追加されたToDo文字列を返す
          return $('#todo-list li:last').find('label').text();
        }, function (addedTodoText) {
          addedTodoText.should.equal(todoText2);
        })
        .run(done);
  });

  it('複数追加したTODOの数が表示されること', function (done) {
    var todoText = 'テストToDo';

    nightMare
        .type('#new-todo', todoText)
        .evaluate(function () {
          var e = $.Event('keypress');
          e.which = 13;
          // エンターキー押下
          $('#new-todo').trigger(e);
        })
        .type('#new-todo', todoText)
        .evaluate(function () {
          var e = $.Event('keypress');
          e.which = 13;
          // エンターキー押下
          $('#new-todo').trigger(e);
        })
        .evaluate(function () {
          return $('#todo-count').find('strong').text();
        }, function (todoCount) {
          todoCount.should.be.equal('2');
        })
        .run(done);
  });

  it('追加したTODOを削除できること', function (done) {
    var todoText = '削除対象TODO';

    nightMare
        .type('#new-todo', todoText)
        .evaluate(function () {
          var e = $.Event('keypress');
          e.which = 13;
          // エンターキー押下
          $('#new-todo').trigger(e);

        })
      // TODO削除ボタンをクリックする
        .click('#todo-list .destroy')
        .evaluate(function () {
          return $('#todo-list li:last').find('label').text();
        }, function (addedTodoText) {
          // TODO文字列が存在しないこと
          addedTodoText.should.be.ng;
        }).run(done);
  });

  it('追加したTODOを完了にできること', function (done) {
    var todoText = '完了対象TODO';

    nightMare
        .type('#new-todo', todoText)
        .evaluate(function () {
          var e = $.Event('keypress');
          e.which = 13;
          // エンターキー押下
          $('#new-todo').trigger(e);
        })
      // 完了チェックをクリックする
        .click('#todo-list .toggle')
        .evaluate(function () {
          return $('#todo-list li:last').hasClass('completed');
        }, function (isCompleted) {
          // TODOが完了になっていること
          isCompleted.should.be.ok;
        })
        .run(done);
  });

  it('追加したTODOを完了すると、カウントが0になること', function (done) {
    var todoText = '完了対象TODO';

    nightMare
        .type('#new-todo', todoText)
        .evaluate(function () {
          var e = $.Event('keypress');
          e.which = 13;
          // エンターキー押下
          $('#new-todo').trigger(e);
        })
      // 完了チェックをクリックする
        .click('#todo-list .toggle')
        .evaluate(function () {
          return $('#todo-count').find('strong').text();
        }, function (todoCount) {
          todoCount.should.be.equal('0');
        })
        .run(done);
  });
});

```
テストコード中で使用している関数は、Nightmareの[API一覧](https://github.com/segmentio/nightmare#api)を参照してください。

今回、テスト対象のアプリがjQueryを読み込んでいるので、しれっとテストコード中でもjQueryを使用しています。

Nightmareの[テストコード](https://github.com/segmentio/nightmare/blob/master/test/index.js)では、テスト対象のページがjQueryを読み込んでいないため、goto()で画面遷移した後に`inject('js', 'test/files/jquery-2.1.1.min.js')`を追加して、動的にjQueryを読み込ませていました。

## テスト実行

テストコードの実行自体は、

```
> mocha
```
でも良いですが、package.jsonを`npm init`で生成する時に`test command: (mocha)`としておくと、

```
> npm test
```

でmochaが実行され、かつ見やすく整形されたテスト結果がコンソールに表示されます。

## テスト実行結果
`npm test`の実行結果は下記の通りです。

```zsh
> nightmare-sample@1.0.0 test C:\Users\**\nightmare-sample
> mocha



  画面遷移
    √ TODOリストが表示できること (6867ms)

  基本動作確認
    √ TODO追加ができること (8651ms)
    √ 追加したTODOの数が表示されること (10224ms)
    √ 複数のTODO追加ができること (8804ms)
    √ 複数追加したTODOの数が表示されること (8176ms)
    √ 追加したTODOを削除できること (8303ms)
    √ 追加したTODOを完了にできること (8552ms)
    √ 追加したTODOを完了すると、カウントが0になること (8456ms)


  8 passing (1m)

```
# 所感
APIがシンプルで、テストコードが書きやすいという印象でした。ただ、テスト結果を見ての通り、非常にテストが遅いのが難点ですね…。並行して実行したいところです。

また、NightmareのAPIに渡す関数内から外部を参照できなかったため、同じ処理を何度も書かないといけなかったのが残念でした（エンターキー押下処理とか）。どうにか工夫すればできるのかな…。

# 参考
- [Nightmare](http://www.nightmarejs.org/)
- [segmentio/nightmare · GitHub](https://github.com/segmentio/nightmare)