karmaで始めるコンポーネントベースのTDD
高いところがダメな小飼です。再来週飛行機に乗ります。
さて、先日Angular1.x系のSPAにユニットテストを導入した話で、作成中のコンポーネントだけをブラウザに描画して、TDDっぽく開発を進めていく手法について触れました。
今回はその具体的な手法について触れたいと思います。
(あまり類似の記事を見たことがないのですが、もしかしたら記事にするまでもないことだったりして...)
ヘッドレスなテストへの不安
PhantomJS
のようなヘッドレスブラウザやjsdom
のような擬似ブラウザ環境でのテストは、実行が軽い反面、テストしたコンポーネントが実際にどういう動きをしているのか?どんな風に描画されているのかに確信を持ちづらいところがあります。
どうにかして、自分が書いているテストが実際にどんなコンポーネントをブラウザに描画するのかを確認出来れば安心できるのですが...
ところでテストランナーのkarma
では、PhantomJS
用に書いたテストコードを、追加設定不要でそのままchrome/firefox
などでも実行できる仕組みがあります。
ブラウザでテスト対象のコンポーネントを描画する
まず最初にkarma
がどんな構成でテストを実行しているのかを簡単にさらってみたいと思います。
図のように、karma
はkarmaランナープロセスがテスト用のサーバを起動し、karma
が用意したhtml
ファイルにテストファイルをブラウザが読み込んで、テストを逐次実行していく構成になっています。
この際テストサーバにアクセス(デフォルトだとhttp://localhost:9876/
)すると、karma.conf.js
で設定したテストのステータスが確認できます。
ここでDEBUG
ボタンのリンク先である/debug.html
ページを読み込みコンソール画面を開くと、ブラウザの内部でPhantomJS
用に設定したのと同じテストが走っているのが確認できます。
ロジックにまつわるテストのみを書いている場合、このページには何も表示されません。
しかしテスト対象のコンポーネントをwindow
オブジェクトに挿入してあげることで、実際にコンポーネントをブラウザに描画してテストすることができます。
こんな感じでコンポーネントをwindow
オブジェクトに渡してあげます。
// 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
は一意でなくなってしまいます)
実際のテストコードはこんな感じになります。
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ファイルや画像ファイルなんかはそのままではテスト環境で読み込まれないという事です。
コンポーネントのスタイルも合わせて確認するためには、こんな感じの設定を追加してあげる必要があります。
// 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
を使用するのが便利です。
こんな感じでシミュレートできます。
$('sample-component').simulate('drag', { dx: 15, dy: 0 });
まとめ
コンポーネントをブラウザに実際に描画しつつテストする手法についてご紹介しましたが、いかがでしたでしょうか。
テストファーストで作るので、責任が小さくシンプルなコンポーネントを作りやすいく、かつCSSも他のコンポーネントに依存せずに作ることになるので、フロントエンドのコンポーネント作成手法としてはわりと気に入って使っています。
また、DOMベースでのテストなので、FW・ライブラリに縛られずテストできる(いつでも同じ感じに書ける)ところもうれしいです。
(弊社のReact.js
を導入している別プロジェクトでも同様の手法でテストを書いています)
コンポーネントベースのテストは、サイズを小さくシンプルにまとめやすいので、テスト変更の負荷が少ない(負債化しづらい)というメリットもあり、今後もこの手法を取り入れてテストを書いていきたと思っています。
今回ご紹介したTDD手法を取り入れ、品質の見える化を実現したサービス開発をご検討の企業様は、是非MMMにご相談下さいませ!