karmaで始めるコンポーネントベースのTDD

高いところがダメな小飼です。再来週飛行機に乗ります。

さて、先日Angular1.x系のSPAにユニットテストを導入した話で、作成中のコンポーネントだけをブラウザに描画して、TDDっぽく開発を進めていく手法について触れました。
今回はその具体的な手法について触れたいと思います。
(あまり類似の記事を見たことがないのですが、もしかしたら記事にするまでもないことだったりして…)

ヘッドレスなテストへの不安

PhantomJSのようなヘッドレスブラウザやjsdomのような擬似ブラウザ環境でのテストは、実行が軽い反面、テストしたコンポーネントが実際にどういう動きをしているのか?どんな風に描画されているのかに確信を持ちづらいところがあります。

どうにかして、自分が書いているテストが実際にどんなコンポーネントをブラウザに描画するのかを確認出来れば安心できるのですが…
ところでテストランナーのkarmaでは、PhantomJS用に書いたテストコードを、追加設定不要でそのままchrome/firefoxなどでも実行できる仕組みがあります。

ブラウザでテスト対象のコンポーネントを描画する

まず最初にkarmaがどんな構成でテストを実行しているのかを簡単にさらってみたいと思います。

karmaのアーキテクチャ図

図のように、karmaはkarmaランナープロセスがテスト用のサーバを起動し、karmaが用意したhtmlファイルにテストファイルをブラウザが読み込んで、テストを逐次実行していく構成になっています。
この際テストサーバにアクセス(デフォルトだとhttp://localhost:9876/)すると、karma.conf.jsで設定したテストのステータスが確認できます。
ここでDEBUGボタンのリンク先である/debug.htmlページを読み込みコンソール画面を開くと、ブラウザの内部でPhantomJS用に設定したのと同じテストが走っているのが確認できます。

ロジックにまつわるテストのみを書いている場合、このページには何も表示されません。
しかしテスト対象のコンポーネントをwindowオブジェクトに挿入してあげることで、実際にコンポーネントをブラウザに描画してテストすることができます。

こんな感じでコンポーネントをwindowオブジェクトに渡してあげます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Angular.js(1.3)でのコード例
function createAngularElement(componentTag) {
var element;
inject(function ($compile, $rootScope) {
var scope = $rootScope.$new();
element = angular.element(componentTag);
$compile(element)(scope);
});

document.querySelector('body').appendChild(element);
return element;
}
const element = createAngularElement('<sample-component></sample-component>');

// React.jsでのコード例
function createReactElement(virtualDOM) {
const element = document.createElement('div');
ReactDOM.render(virtualDOM, element);

document.querySelector('body').appendChild(element);
return element;
}
const element = createAngularElement(<SampleComponent />);

この時、return elementなどで作成したコンポーネントのDOMへの参照を、テストコードに渡してあげる必要があります。
windowオブジェクトに直接テスト対象のコンポーネントをappendしていくので、テスト毎にコンポーネントへの参照も分けて保持しておく必要があるからです。
(コンポーネントにid要素を指定してgetElementByIdしようとしても、id付きのコンポーネントがいくつもdocumentに描画されるのでidは一意でなくなってしまいます)

実際のテストコードはこんな感じになります。

1
2
3
4
5
6
7
8
9
10
describe('サンプルのテスト', function() {
var element;
beforeEach(function() {
element = createAngularElement('<sample-component></sample-component>');
// element = createAngularElement(<SampleComponent />);
});
it('サンプルコンポーネントが描画できる', function() {
expect(element.querySelector('sample-component').length).toBe(1);
});
});

これで任意のブラウザのhttp://localhost:9876/debug.htmlにアクセスすると、サンプルコンポーネントが描画されています。
(もちろん内部ではテストが実行されています)
これでどんなコンポーネントが描画されているのか、確信を持ってテスト出来ます!
もちろん、clickなどの各種イベントも発行可能です。

その他

アセットファイルのプロキシ設定

karmaの概要を説明する段で、karmaの立てるテストサーバにテストコードが挿入されているという仕組みを説明しました。
別の言い方をすると、CSSファイルや画像ファイルなんかはそのままではテスト環境で読み込まれないという事です。

コンポーネントのスタイルも合わせて確認するためには、こんな感じの設定を追加してあげる必要があります。

1
2
3
4
5
6
7
8
9
// karma.conf.js
files: [
'test/*.js',
'app/style.css',
{ pattern: 'app/img/*', watched: false, included: false, served: true, nocache: false },
],
proxies: {
'/img/*': '/base/app/img/*',
},

CSSファイルはlinkタグで、画像ファイルはテストサーバの/base/app/img/*として読み込まれた後、/img/*としてkarmaが中継して読み込んでくれます。
(fontファイルなんかも同じ感じです)

DOM操作イベントのシミュレーション

前述の通り、clickイベントなんかは生DOM APIでテストを書くことが出来ます。
ただし、テキストの入力やドラッグなんかのイベントは(無理ではないですが)書くのが面倒です。

そこでDOM操作ライブラリの金字塔jQueryを使います。
例えばテキスト入力操作をテストしたいのであれば、jQueryに組み込み済みのイベント($('sample-component').val('テキスト入力').trigger('input'))を発火させます。

また、jQuery本体が発火できない(しづらい)イベント(例えばマウスドラッグ)は、jQueryが自身のテストのために使っているライブラリ(プラグイン)jquery-simulateを使用するのが便利です。

こんな感じでシミュレートできます。

1
$('sample-component').simulate('drag', { dx: 15, dy: 0 });

まとめ

コンポーネントをブラウザに実際に描画しつつテストする手法についてご紹介しましたが、いかがでしたでしょうか。
テストファーストで作るので、責任が小さくシンプルなコンポーネントを作りやすいく、かつCSSも他のコンポーネントに依存せずに作ることになるので、フロントエンドのコンポーネント作成手法としてはわりと気に入って使っています。

また、DOMベースでのテストなので、FW・ライブラリに縛られずテストできる(いつでも同じ感じに書ける)ところもうれしいです。
(弊社のReact.jsを導入している別プロジェクトでも同様の手法でテストを書いています)

コンポーネントベースのテストは、サイズを小さくシンプルにまとめやすいので、テスト変更の負荷が少ない(負債化しづらい)というメリットもあり、今後もこの手法を取り入れてテストを書いていきたと思っています。

今回ご紹介したTDD手法を取り入れ、品質の見える化を実現したサービス開発をご検討の企業様は、是非MMMにご相談下さいませ!

このエントリーをはてなブックマークに追加