テストできないコードをE2Eテストを使ってリファクタリングしよう
ユニットテストがしにくい状態となってるコードをTestiumを使ったE2Eテストを書いてリファクタリングしてみる話です。
例えば、以下のようなjQueryで書いたコードは外(テストコード)から取り出すポイントがないので、ユニットテストを書くのは難しいと思います。(そもそもViewのコードなので)
特定のバージョンでの変更点を簡単に確認できるよう、 「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入ってる)で動かすサンプルを作ったので、それを見ながら解説します。
- azu/testium-seed
- READMEにも同じ解説が書いてあります。
1: インストール devDependencies
このプロジェクトはtestium、Mocha、power-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/
chromeでテストする
先ほどはデフォルトのphantomjsでテストを実行しましたが、chromeなどでもテストを動かせます。
1: .testiumrc
にbrowser = "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としてbrowser
APIを叩いて試せます。
./node_modules/.bin/testium --browser firefox
3: テストを実行する
後は実行するだけです。
npm test
細かい設定は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というプロジェクトで、JavaScriptのリファクタリングを写経をしていましたが、jQueryのベタ書きだとユニットテストを入れるのが結構難しいと思います。
E2Eテストの場合は現状を壊してないかのテストは比較的カンタンに書くことが出来ます(そこから継続して機能追加するとまた別のコストがありますが)
ユニットテストは機能で取り出せないとテスト難しいけど、E2Eテストは面で切り取りできるので既存のものへテスト置きやすい。
— azu (@azu_re) January 21, 2015
最初にあげた美しいプログラムを書く(業務用Webアプリケーション保守編) - TIM Labsを題材に、Testiumを使ったE2Eテストを書いてリファクタリングしたものが以下にあります。
実際に行ったリファクタリングはそれぞれPull Requestになってます。
- Refactoring jQuery by azu · Pull Request #1 · coding-kata/beautiful-code-vs-mr-oldtype
- E2Eテストを書いて記事中にあったリファクタリングを適応した内容
- Refactoring E2E tests to apply page-object pattern by azu · Pull Request #2 · coding-kata/beautiful-code-vs-mr-oldtype
- E2Eテストのリファクタリングをした内容
- Page Objectsパターンを使うことで、テスト内から
this.browser.getElements('input[name="new_version"]')
のようなセレクタを排除して、ページが変更されても更新しやすくなるようにしてます
他にも幾つか同じような構成で写経をしてました。
- coding-kata/todo-app-jquery-to-backbone
- リファクタリングのためのテストのE2EテストをTestiumに移植した内容
- jQueryで書かれたTodoアプリをBackbone.jsベースにリファクタリングしてます。
- 実装はES6で書いてます => ライブラリをES6で書いて公開する所から始めよう | Web Scratch
- coding-kata/monologue
- 以下のを元にjQueryベースのものをBackbone.jsベースにリファクタリングする内容です
- Browserifyでファイルとしてモジュールを分けたりなどもしています。
- The Plight of Pinocchio: JavaScript’s quest to become a real language
- writings/understanding-backbone.md at master · kjbekkelund/writings
またリポジトリを見てみると分かるように全部Travis CIでE2Eテストが動いています。
jQueryで書いたコードをどうやってリファクタリングするかはThe Refactoring Tales - JavaScriptのリファクタリング本を読んだ | Web Scratchで書いたThe Refactoring Talesを読んでみるのもいいかもしれません。
おわりに
まだ、足りない機能や日本語をWebDriver API経由での入力が上手く行かないバグ等(直った)もありますが、Protractorに比べると設定ファイルや実行するために覚える必要があることが少なくて済むのでいいなーという感じがします。
後、やはり同期的なAPIであることやMochaでユニットテストと同じように書けるのが書き心地に影響があって、E2Eテストとユニットテストで脳をスイッチするコストが小さく済むように思えます。
まだgroupon-testium/testium試したことがある人は少ないと思いますが、E2Eテストがユニットテストと同じぐらい手軽に書けるので試してみるといいかもしれません。
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。