ユニットテストがしにくい状態となってるコードをTestiumを使ったE2Eテストを書いてリファクタリングしてみる話です。

例えば、以下のようなjQueryで書いたコードは外(テストコード)から取り出すポイントがないので、ユニットテストを書くのは難しいと思います。(そもそもViewのコードなので)

img

特定のバージョンでの変更点を簡単に確認できるよう、 「Aの列のラジオボタンを選ぶと同じ行より一つ下にあるBの列のラジオボタンを自動で選ぶ」 という補助機能

$(document).ready(function () {
    // seq: シーケンス番号
    $.each(["new_version", "old_version"], function () {
        $("input[name='" + this + "']").each(function (idx, elem) {
            if (idx == 0) $(elem).attr('checked', 'checked');
            $(elem).attr('seq', idx);
        });
    });
    $("input[name='new_version']").change(function () {
        var seq = parseInt($(this).attr('seq'));
        $("input[name='old_version']").eq(seq).attr('checked', 'checked');
    });
});

上記の記事ではこのコードをリファクタリングしていますが、リファクタリングする際に動作が変わってないかの保証がないと不安です。

通常ではユニットテストがその役割に使われると思いますが、元々テストコードがなかったり、上記のような全体が関数で囲まれているとテストから触れないため難しいケースがあります。

このようなケースでもE2Eテストだと内部的なコードがどうなってるかはあまり気にしないで動作のテストが可能になります(Integration Tests、UI Tests 呼ばれ方が色々ある気がしますがどれもイマイチピンとこないので、とりあえずE2Eとします)

この記事では、E2EテストフレームワークであるTestiumの紹介とどうやってリファクタリングするかについて紹介します。

Testiumとは

TestiumはWebDriver APIを使いブラウザを操作して出来るテストフレームワークです。同様のツールとして以下のようなものがあります。

WebDriver APIに対応してるブラウザを実行環境として使えるので、現在だとPhantomJS/Chrome/Firefox/IE等でE2Eテストを動かすことが出来ます。

Testiumは同期的なWebDriver APIのラッパを持っているため、.click()といったブラウザを操作する処理を同期的に書くことができるのが大きな特徴です。

var inputNew = browser.getElement('input');
inputNew.click();// クリックの処理が同期的になってる
assert(inputNew.get("value"), "expected value");

他のE2Eテストフレームワークは非同期で呼び出すWebDriver APIのラッパーを使っています。 そのためNightwatch.jsのようなDSL的なメソッドチェーンやProtractorのようなPromiseを吸収するような仕組みが必要になります。(ProtractorがJasmineと密接なのもexpect(promise)という形のassetionレベルでの非同期対応が必要となるためです)

また、TestiumはMocha上でテストを書くことができるのでpower-assertなど既存のものがそのまま使うことが出来ます。

Mocha連携の仕組みも、before(injectBrowser) という形で、mochaが用意してるテストのイベント(テスト開始時、終了時)に処理を登録したり、this.browserを追加するだけなので、Mochaに強く依存してるわけではないです。(今のところMochaしかないですが、自分で書くのもそんなに難しい感じではないと思います。

実際に書いてみると普段書くユニットテストと似たような感覚で書くことができ、this.browserにブラウザを操作するTestiumのAPIが用意されています。

var injectBrowser = require('testium/mocha');
var assert = require("power-assert");
var browser;
describe("app-test", function () {
    var text = 'todo text';
    before(injectBrowser());
    beforeEach(function () {
        browser = this.browser;
        this.browser.navigateTo("/");
    });
    context("when テキストボックスに文字を入れて送信した時", function () {
        beforeEach(function () {
          // テキストを入力して送信ボタンをクリック
            browser.setValue('.todoText', text);
            browser.click('.todoBtn');
        });
        it("should li要素が作成されている", function () {
            var list = browser.getElements('.todoList li');
            assert(list.length > 0); // assertが普通に利用できる
        });
        it("should リストアイテムのテキストは送信したものと一致している", function () {
            // testiumが用意してるassertionも幾つかある
            browser.assert.elementHasText('.todoList li', text); 
        });
    });
});

面白いところとしては、デフォルトでテストが失敗した時にスクリーンショットが保存されてるようになっています。 また、スクリーンショット同士の比較してのテストするAPIも持っているため、画面が変化してないかのチェック等も可能です。

Testiumを動かしてみよう

Testiumを最小構成(といいつつpower-assert入ってる)で動かすサンプルを作ったので、それを見ながら解説します。

site

screenshot-error

1: インストール devDependencies

このプロジェクトはtestiumMochapower-assertを使っているので以下のような感じで必要なものをインストールします。

また実行環境としてphantomjsを使いたいのでphantomjsもインストールします(グローバルにインストールしてる場合は別に必要ありません)

npm install --save-dev mocha power-assert intelli-espower-loader testium http-server phantomjs

2: .testiumrcの作成

Testiumの設定ファイルである.testiumrcファイルを作成して、プロジェクトルートにおきます。

ini形式で書く設定ファイルで、launch = trueと書くとnpm testという感じでテストを実行した時に、自動的にnpm startで指定したローカルサーバを立てて、設定したブラウザ(デフォルトではphantomjs)を起動して実行出来ます。

; defaults to false, `npm start`s the app
launch = true

別のローカルサーバを立てて繋ぎたい場合は設定でportを指定したりしてつなぎます。(参考: coding-kata/monologue)

[app]
port=8989

3: HTTPサーバを立てる

HTMLを表示してアクセスする場合にもローカルサーバを立ててそこに繋いだ方がいいので、nodeapps/http-serverを使ってHTTPサーバを立てます。

npm install -D http-server

でインストールしていれば、以下のように書いておけばプロジェクトルートを元に http://localhost:port でアクセス出来ます。

  "scripts": {
    "start": "http-server",
    "test": "mocha test/*.js"
  },

4: E2Eのテストケースを書く

詳しくはapp-test.jsを見てみるといいですが、以下のような形でテストを書きます。

this.browser.navigateTo("/")でローカルサーバにアクセスして、ブラウザを操作してテストをします。

"use strict";
var injectBrowser = require('testium/mocha');
var assert = require('power-assert');

describe("index.html", function () {
    before(injectBrowser());// <= integrate testium 
    beforeEach(function () {
        this.browser.navigateTo('/');// move to `"/"` which is served by http-server
    });
    it("then output filled with text of this option", function () {
        var firstInput = this.browser.getElement('input[name="framework_name"]');
        // click
        firstInput.click();
        // assert 
        var testFrameWorkName = firstInput.get("value");
        var output = this.browser.getElement("#js-output");
        var result = output.get("text");
        assert(result.indexOf(testFrameWorkName) >= 0);
    });
});

5: テストを実行する

後はMochaでテストを実行するのと同じです。

npm test
# alias to
mocha --require intelli-espower-loader test/

Success tests

chromeでテストする

先ほどはデフォルトのphantomjsでテストを実行しましたが、chromeなどでもテストを動かせます。

1: .testiumrcbrowser = "chrome"を追加する

; defaults to false, `npm start`s the app
launch = true
browser = "chrome"

2: Download chromium-driver

Testiumにはchrome-driver等をダウンロードするコマンドがあるので、そのコマンドを使います。

$ ./node_modules/.bin/testium --download-selenium

Tips:

TestiumコマンドにはREPL機能があり、以下のようなコマンドを実行するREPLとしてbrowserAPIを叩いて試せます。

./node_modules/.bin/testium --browser firefox

3: テストを実行する

後は実行するだけです。

npm test

chrome-e2e

細かい設定はConfigurationにかいてありますが、この設定書式をini形式(Key=value)に直す必要があることには注意して下さい。

例えば、

app:
  # A port of 0 means "auto-select available port"
  port: process.env.PORT || 0

は以下のような感じになると思います。

[app]
port=0

補足

またTestiumは全てのWebDriver APIラッパが実装されてないので、足りないものはwebdriver-http-syncに一覧があるので実装してみるといいかもしれません。

E2Eテストとリファクタリング

coding-kata

最近coding-kataというプロジェクトで、JavaScriptのリファクタリングを写経をしていましたが、jQueryのベタ書きだとユニットテストを入れるのが結構難しいと思います。

E2Eテストの場合は現状を壊してないかのテストは比較的カンタンに書くことが出来ます(そこから継続して機能追加するとまた別のコストがありますが)

最初にあげた美しいプログラムを書く(業務用Webアプリケーション保守編) - TIM Labsを題材に、Testiumを使ったE2Eテストを書いてリファクタリングしたものが以下にあります。

実際に行ったリファクタリングはそれぞれPull Requestになってます。

他にも幾つか同じような構成で写経をしてました。

またリポジトリを見てみると分かるように全部Travis CIでE2Eテストが動いています。

Build Status

jQueryで書いたコードをどうやってリファクタリングするかはThe Refactoring Tales - JavaScriptのリファクタリング本を読んだ | Web Scratchで書いたThe Refactoring Talesを読んでみるのもいいかもしれません。

おわりに

まだ、足りない機能や日本語をWebDriver API経由での入力が上手く行かないバグ等(直った)もありますが、Protractorに比べると設定ファイルや実行するために覚える必要があることが少なくて済むのでいいなーという感じがします。

後、やはり同期的なAPIであることやMochaでユニットテストと同じように書けるのが書き心地に影響があって、E2Eテストとユニットテストで脳をスイッチするコストが小さく済むように思えます。

まだgroupon-testium/testium試したことがある人は少ないと思いますが、E2Eテストがユニットテストと同じぐらい手軽に書けるので試してみるといいかもしれません。