Web フロントエンドの開発には Chromenpm が欠かせないものとなっています。
(※ Eq でも misupopoimage-ghost という画像のリサイズツールを公開しているので使ってみてください。)

そのため、ローカルの開発環境に Chrome や npm を使ってテスト環境が構築できれば、新たにアプリケーションをインストールする必要が無いというメリットがあります。
そこで前回前々回Headless Chromechrome-remote-interface の使い方を紹介してきました。

しかし、これで追加のインストール無しに環境を構築できるのはローカル環境だけです。
たとえば Jenkins を使った CI 環境を構築する場合には、別途 Chrome や Chromium をインストールする必要があります。
これでも十分、楽ではありますが npm install コマンドを叩くだけの場合と比べると、やはり少々面倒です。

だったら npm からインストール可能な Nightmare を使ってみたらどうだろう?
そう考えて、Protractor のサイトにあるテストのサンプルと同じ内容のテストを、今回は Nightmare を使って書いてみることにしました。


Nightmare

Nightmare の紹介を見ると「A high-level browser automation library」とあります。
ここでいうブラウザは Chrome や Firefox など私達が普段から使っているようなブラウザではなく Electron を指します。

Electron は Node.js と Chromium の技術を使って Web の技術でデスクトップアプリケーションを開発できるフレームワークです。
Nightmare ではこれをブラウザとして使用し JavaScript から操作ができるようになっています。

Chromium が使われているということは、レンダリングされる内容や JavaScript の動作などは Headless Chrome と同じはずです。
また Headless Chrome ではできなかった、クエリセレクターで要素を指定してクリックイベントを発生させるといったことが可能です。

使い方については、Nightmare のリポジトリに含まれる README.md を見るだけで判ると思います。
また nightmare-examples というリポジトリでは使用例や、ドキュメントが公開されています。


環境構築

今回も vagrant を利用して Ubuntu 16.04 上に環境を構築し、次のような環境を構築しました。

Name Version
OS Ubuntu Linux 16.04(Xenial)64bit
Node.js 8.1.4
nightmare 2.10.0

環境構築に利用した Vagrantfile と Ansible の playbook、テストのソースコードは GitHub で公開しています。
nightmare-test を clone して vagrant up を実行するだけで、簡単に同じ環境を構築して、試していただけると思います。


テスト

Nightmare では文字を入力する type や、クリックイベントを発生させる click などのメソッドが用意されています。
そのため chrome-remote-interface のように自分でメソッドを用意しなくても、簡単にテストを書き始めることができます。

注意点があるとすれば evaluate が返した値を then で取得した後、さらに click などのメソッドをつなげることが、できない点でしょうか。
この辺りは、上で紹介した nightmare-examples に含まれるドキュメントの「Asynchronous operations and loops」が参考になると思います。

'use strict';

const expect = require('expect.js');
const nightmare = require('nightmare');

describe('Nightmare test', function () {
    describe('Protoractor test sample', function () {
        describe('Should complete test', function (done) {
            it('Multi evaluate', function (done) {
                (async () => {
                    const client = nightmare({show: false}),
                        text = 'write first protractor test';

                    client.goto('https://angularjs.org/')
                        .type('input[ng-model="todoList.todoText"]', text)
                        .click('input[value="add"]')
                        .evaluate(function () {
                            return document.querySelectorAll('li[ng-repeat="todo in todoList.todos"]');
                        }).then(function (result) {
                            expect(Object.keys(result)).to.have.length(3);

                            return client.evaluate(function () {
                                const elements = document.querySelectorAll('li[ng-repeat="todo in todoList.todos"]');

                                return elements[2].querySelector('span').innerText;
                            });
                        }).then(function (result) {
                            expect(result).to.be(text);

                            return client.click('li[ng-repeat="todo in todoList.todos"]:nth-child(2) input')
                                .evaluate(function () {
                                    return document.querySelectorAll('.done-true');
                                });
                        }).then(function (result) {
                            expect(Object.keys(result)).to.have.length(2);

                            client.end().then(function () {
                                done();
                            });
                        });
                })();
            });
        });
    });
});

上の例ではプログラムの流れを、もとのコードに合わせているため evaluate メソッドが複数回、呼ばれています。

好みの問題ではありますが evaluate に書かれている内容は、次のようにまとめることもできます。
値を検証する部分が 1 箇所にまとまるので、こちらのほうが都合のいいこともあると思います。

client.viewport(1024, 768)
    .goto('https://angularjs.org/')
    .type('input[ng-model="todoList.todoText"]', text)
    .click('input[value="add"]')
    .evaluate(function () {
        const elements = document.querySelectorAll('li[ng-repeat="todo in todoList.todos"]');
        elements[2].querySelector('input').click();

        return {
            elements: elements.length,
            completes: document.querySelectorAll('.done-true').length,
            text: elements[2].querySelector('span').innerText
        };
    }).then(function (result) {
        expect(result.elements).to.be(3);
        expect(result.completes).to.be(2);
        expect(result.text).to.be(text);

        client.end().then(function () {
            done();
        });
    });

ヘッドレス環境で動かすときの問題

テストが書けたので動作を確認をしようと、vagrant 環境に入り、次のような感じでテストを動かしてみました。

# ローカル環境から Vagrant 環境に入る
vagrant ssh

# Vagrant 環境でテストを実行する
cd /vagrant
yarn install
mocha --no-timeouts test.js

しかし、テストは途中で落ちてしまいました。
画面の表示はこうです。

Nightmare test
    Protoractor test sample
      Should complete test

it で落ちているのは想像がつきますが、エラーメッセージはなく、echo $? の結果も 0 でした。


xvfb をインストールするだけでは動かなかった

これだけでは何が悪いかわからないので、同じような問題が報告されていないか GitHub の issue を確認してみました。
すると「Running Nightmare headlessly on Linux #224」が見つかりました。

ヘッドレスな環境で動かすためには xvfb を使えとあります。
どうやら Nightmare を使っても npm だけで簡単に環境を構築するということはできないようです。

xvfb をインストールするのであれば Chromium をインストールするのと手間は、それほど変わりません。
しかし、ここまでやったのだからと xvfb をインストールして、動かしてみました。

xvfb-run --server-args='-screen 0 1024x768x24' mocha --no-timeouts ./test.js

結果は、このとおりで、何も変わりませんでした。
終了コードも 0 のままです。

Nightmare test
    Protoractor test sample
      Should complete test

xvfb 以外に必要なライブラリ

xvfb を使えばテストが動くと思っていたのですが、そうでは無かったようです。

では、何が悪いのでしょうか。
さきほどの issue を参考に DEBUG 環境変数を指定して、もう 1 度コマンドを実行してみました。

DEBUG=nightmare* xvfb-run --server-args='-screen 0 1024x768x24' mocha --no-timeouts ./test.js

結果はこうです。

Nightmare test
    Protoractor test sample
      Should complete test
nightmare queuing process start +0ms
nightmare queueing action "goto" for https://angularjs.org/ +2ms
nightmare queueing action "type" +1ms
nightmare queueing action "click" +0ms
nightmare queueing action "evaluate" +0ms
nightmare running +0ms
nightmare electron child process exited with code 127: command not found - you may not have electron installed correctly +14ms
nightmare electron child process not started yet, skipping kill. +1ms

エラーメッセージから Electron が起動できていないことが判ります。
Electron のエラーを見るには DEBUG=electron* とすると良さそうなことは issue から判ります。

そこで、さきほどのコマンドを、こう変更して実行してみました。

DEBUG=electron* xvfb-run --server-args='-screen 0 1024x768x24' mocha --no-timeouts ./test.js

結果は、次のとおりです。
原因と思われるエラーメッセージが表示されています。

Nightmare test
    Protoractor test sample
      Should complete test
electron:stderr /vagrant/node_modules/electron/dist/electron: error while loading shared libraries: libgtk-x11-2.0.so.0: cannot open shared object file: No such file or directory +0ms

Electron が起動できていなかった原因は libgtk-x11-2.0.so.0 が無かったことでした。
xvfb をインストールするだけでテストが動くようになると思っていたのですが、他にもライブラリが必要だったようです。

そこで apt コマンドで libgtk2.0-0 をインストールして、再度テストを動かしてみました。
しかし、まだエラーになってしまっています。

electron:stderr /vagrant/node_modules/electron/dist/electron: error while loading shared libraries: libXtst.so.6: cannot open shared object file: No such file or directory +0ms

ここからは、テストが動くようになるまで、必要なライブラリをインストールして DEBUG=electron* を付けてテストを実行することを繰り返しました。
最終的にテストを動作させるのに xvfb 以外に必要だったライブラリは、次の 6 つです。

  • libgtk2.0-0
  • libxtst6
  • libxss1
  • libgconf-2-4
  • libnss3
  • libasound2

まとめ

npm でインストールができる Nightmare を使えば環境構築が楽になるかと思ったのですが、Electron を動かすのには、思ったよりも色々なライブラリが必要でした。
簡単に環境が構築できることを重視するなら Headless Chrome + chrome-remote-interface の方が良さそうです。

ただしテストの書きやすさでいえば Nightmare の方が、大分、書きやすい印象です。
環境構築の手間も含め Nightmare は Selenium に近い印象を受けました。
Headless Chrome のときもそうでしたが、すでに Selenium を使ったテストがあれば、そこから乗り換えるメリットは無さそうです。

とはいえ、手元のマシンにテストの環境を作りたいのであれば、Selenium や Headless Chrome よりもお手軽です。
WebDriver や Headless Chrome は利用する前に自分で立ち上げる必要がありますが Nightmare では、その必要がありません。
goto メソッドを呼べば自動で Electron が立ち上がりますし、end メソッドで終了します。

他にも Headless Chrome は Headless ではない Chrome が、すでに立ち上がっていると起動ができません。
Chrome をメインのブラウザとして使っている場合、これが問題になることもあると思います。

結局のところ、どれも一長一短で、これがベストと、いえるものはありませんでした。
色々なブラウザで試す必要があれば Selenium を使うなど、要件に合わせて使い分ける必要がありそうです。

後は、好みの問題でしょうか。
個人的には、ローカル環境の構築がしやすく、テストも書きやすい Nightmare が良さそうに思いました。

コメントの投稿