個人的に趣味で Chrome Extension の開発をしていますが、最近いろいろとノウハウも溜まってきたので Chrome Extension の CI について少しまとめてみようと思います。
目次
- はじめに
- Chrome Extension のテストを書く
- Chrome Extension のテストを実行する
- Chrome Extension を CI する
- CI サービス Wercker
- Wercker にリポジトリを登録する
- wercker.yml
- Step の作り方
- バッヂ
はじめに
Google Chrome を vim のようにキーボードで操作できるようにする Hometype という Chrome Extension を作っていますが、この記事は Hometype でやってきたことベースに話を展開していきます。
テストのやり方や、使用するライブラリ、サービスについては、どれが正しくてどれが間違っているのか、ということは気にしすぎると先に進めません。そういうものはケースによってどれが最適なのか変わってくると思いますし、ここで紹介すること全てが正しいとも限らなくて、あくまで一例に過ぎないと思うので、ネットに転がっていることを過信せずに、まずは手を動かして実際にやってみるのがオススメです。やっていくうちに自分の中でノウハウが溜まり、ベストプラクティスに昇華されていきます。
現在 Hometype は Jasmine + Karma + Phantomjs + Wercker という構成で CI をしており、これらの紹介や使い方が主な記事の内容になっていきますので、この辺りに興味がある人に楽しんで読んでもらえることを目標に書いていきます。
Chrome Extension のテストを書く
Chrome Extension もある程度規模が大きくなってくるとテストを書きたくなってくると思います。通常の JavaScript のテストと同様 Chrome Extension のテストもテストフレームワークを使ってテストを書くことができますので、まず最初に少しその話をしていきたいと思います。
何をテストするのか
Chrome Extension のテストを書くといっても、何も考えずに開発を進めていくとテストの書きにくいプロダクトになってしまいます。Chrome Extension に限った話ではありませんが、テストを書きやすくするためには、適切に機能をモジュールに分割して、なるべく副作用のないメソッドを書くことを心がけないといけません。
Chrome Extension では、タブ中に表示されているページのコンテキストで JavaScript を実行できる Content Scripts の中で、要素を取得したり、要素を書き換えたり、イベントハンドラを仕込んだり、様々な操作を DOM に対してやっていくのですが、そうすると DOM と密接なプログラミングになってしまいがちです。普段 jQuery などを使って Web ページを開発しているとわかると思いますが、DOM と密結合したソースコードだとユニットテストが少々やりにくいため(そういうのはユニットテストではなくて Selenium や Capybara などを組み合わせてエンドツーエンドテストをやったりしますよね)、なるべく DOM 操作部分と実際のロジック部分は分けて書いていきます。
Hometype でも Content Scripts を通して DOM 操作を行う処理が多くありますが、それら DOM 操作とは別に、ロジックを実行する処理はなるべくモジュールに切り出して、そのモジュールに対してユニットテストを書くようにしています。
テストフレームワーク
テストを書く時は多くの場合、その言語用にテストフレームワークが準備されています。JavaScript も例にもれずいくつかのテストフレームワークがありますので、その中から適したものを選んでいきましょう。JavaScript のテストフレームワークは Jasmine や JsUnit、QUnit などがあり、その中で Hometype では Jasmine を採用しています。
Jasmine は JavaScript の BDD テストフレームワークで RSpec と似たような記法でテストを書けます。現在はバージョン 1.3 と 2.0 がありますが、新しく使いはじめるのであれば 2.0 一択でいいと思います。
Hometype で Jasmine を使っている理由としては
- JsUnit は開発が停止している(Jasmine へ移行した模様)
- QUnit は非常にシンプルで導入しやすいが、ある程度の規模のテストを書くとなると役不足に感じた(個人的感覚)
describe
it
expect
beforeEach
といった DSL や、豊富なマッチャを使って読みやすいテストを書くことができるspyOn
でオブジェクトをスタブ化できる- Hometype 以外のプロダクトでも使ったことがあり、経験があった(ここが結構でかいかも)
といったことが挙げられます。JsUnit や QUnit でできる事はだいたい Jasmine でも出来ますし、先の 2 つよりは Jasmine の方に流れが来ているような感じはします。ただ、こういうのはケース・バイ・ケースだと思っているので、簡単なテストでよければ QUnit を選択するのもありだと思いますし、テスト書かなくて良いようなものもあると思います。
Jasmine のインストール
それでは Jasmine をインストールしてみましょう。Jasmine には Standalone 版と RubyGem 版という 2 つがあって、それぞれ使い方が違います。RubyGem 版は Ruby on Rails や Ruby を使った開発の際に使うと便利なものですので Chrome Extension でテストする場合は Standalone 版を使います。
Jasmine の Standalone 版は github からダウンロードできます。
※ちなみに Jasmine のリポジトリの中には各バージョン毎の圧縮ファイルが置いてあるディレクトリがあるので 2.0 以外もそこからダウンロードすることが出来ます。https://github.com/pivotal/jasmine/tree/master/dist
ダウンロードした zip ファイルを解凍すると以下のような構成になっているのが確認できます。
MIT.LICENSE
ライセンス説明書SpecRunner.html
テスト実行用ファイルlib/
Jasmine 本体spec/
Jasmine を使って書かれたテストのサンプルsrc/
テスト対象のファイル
ここで spec/
と src/
は Jasmine のサンプルになってますので、中身をのぞいてみると Jasmine でどんなテストが書けるのか雰囲気をつかめると思います。Chrome Extension のテストを書くにあたって必要なものは SpecRunner.html
と lib/
だけです。
解凍したディレクトリの中から SpecRunner.html
と lib/
だけ取り出して Chrome Extension のディレクトリにコピーしておきましょう。Hometype では spec/
以下にそれらのファイルを配置しています。
Jasmine でテストを書いてみる
テストを書いていきましょう。Hometype を例に取ってみると、たとえば
js/executer.js
コマンド実行のためのオブジェクトspec/js/executer_spec.js
上記オブジェクトのテストファイル
という風に、テスト用のファイルにはテスト対象ファイル名に _spec
がつくようにルール決めをしています。Jasmine は、必ずこうしなければテストが実行できない、といったことはないのですが RSpec の文化を真似してる感じです。
テストの中身はたとえばこのように RSpec 風に書くことができます。どういう書き方が出来るのかは公式サイトに詳しく書いてありますので、参考にするとよいです。
describe('Executer', function() {
var executer;
beforeEach(function() {
executer = new Executer(ModeList.NORMAL_MODE, 'a');
});
it('should be fixed a candidate', function() {
expect(executer.fixedCandidate()).toBe(true);
expect(executer.noCandidate()).toBe(false);
});
});
テストを書き終わったら次は SpecRunner.html
を準備します。
SpecRunner.html
は body の中身は空っぽで、重要なのは head タグの中の JavaScript の読み込みです。テストの実行に必要なファイルをここで全部読み込みます。head タグのスクリプト読み込み部分だけ抜粋すると、以下の様な構成になります。
<!-- Jasmine 本体を読み込みます -->
<!-- パスは SpecRunner.html からの相対パスで -->
<script type="text/javascript" src="lib/jasmine-2.0.0/jasmine.js"></script>
<script type="text/javascript" src="lib/jasmine-2.0.0/jasmine-html.js"></script>
<script type="text/javascript" src="lib/jasmine-2.0.0/boot.js"></script>
<!-- テスト対象のソースファイルを読み込みます -->
<!-- パスは SpecRunner.html からの相対パスで -->
<script type="text/javascript" src="../js/executer.js"></script>
<!-- テストを読み込みます -->
<!-- パスは SpecRunner.html からの相対パスで -->
<script type="text/javascript" src="../spec/js/executer_spec.js"></script>
ここまで準備して SpecRunner.html
をブラウザで開いてみると以下のように結果が表示されます。
Chrome Extension でテストを書き始める準備が整いました。
JavaScript APIs をモックする
Chrome Extension は様々な機能を API として提供してくれます。たとえば Content Scripts と Background Page とでやりとりをするための chrome.runtime やローカルストレージ機能の chrome.storage などがあります。これらの API は Chrome から提供されるものなので、もちろん Chrome Extension 以外では動作しません。Jasmine でテストを実行する際も例外ではありません。
そこでテストを実行する際、これらの API は全てモック化してあげる必要があります。中身は特に実装する必要はありません。テストの実行に支障がないようにすることだけを考えて適当にオブジェクトを作っていきます。
たとえばこんなソースコードがあったとすると、そのまま Jasmine の SpecRunner.html でテストを実行しようとすると TypeError: ‘undefined’ is not an object といったエラーが出てしまいます。Jasmine からの実行では chrome
というオブジェクトが定義されておらず undefined になっており、その undefined に対して runtime
というプロパティを参照しようとしてエラーになります。
// Background Page と通信
chrome.runtime.sendMessage({ command: 'commandName' }, function() {
// ストレージに保存
chrome.storage.sync.set({ 'options': params });
// 他の何かの処理
// ...
});
ですので spec/lib/mocking.js
のようなファイルを作って、そこに以下のようなコードを書き込みます。
chrome = {
storage: {
sync: {
set: function() {
}
}
},
runtime: {
sendMessage: function(params, callback) {
callback();
}
}
};
ただ単純にオブジェクトとそのプロパティを API に合うように作っていくだけです。これを SpecRunner.html の最初のほうで読み込むようにします。
<!-- Jasmine 本体を読み込みます -->
<!-- パスは SpecRunner.html からの相対パスで -->
<script type="text/javascript" src="lib/jasmine-2.0.0/jasmine.js"></script>
<script type="text/javascript" src="lib/jasmine-2.0.0/jasmine-html.js"></script>
<script type="text/javascript" src="lib/jasmine-2.0.0/boot.js"></script>
<!-- これを追加で読み込む -->
<script type="text/javascript" src="lib/mocking.js"></script>
こうすることで Jasmine からの実行でも API をモックして処理を止めること無くテストを実行することが出来ます。
HTML Fixture を読み込む
なるべく DOM に依存しないコードを書くと言っても、どうしても 100% そうはできないのが現実です。HTML のある要素がある前提で書かれたコードなどをテストしたい場合も出てきます。そのような場合 Jasmine では DOM Fixture をロードしてからテストを実行させることができます。
たとえば、以下のような要素に対して、次のようなコードがあったとします。
<div id="hoge"></div>
function fuga(message) {
document.getElementById('hoge').innerText = message;
}
この場合、引数の message
が ID hoge
要素に対してきちんとテキストとしてセットされているかどうかをテストしたいはずです。こういう時は jasmine-jquery を使います。これは jQuery 用のマッチャを追加してくれるライブラリですが、その他に HTML Fixture をロードする機能ももっています。今回はその Fixture をロードする機能を使っていきます。
ダウンロードした jasmine-jquery.js
は適宜 spec/lib/
以下などに配置して SpecRunner.html から読み込みます。また、このライブラリは jQuery に依存しているので jQuery も読み込んでおきます。
<!-- Jasmine 本体を読み込みます -->
<!-- パスは SpecRunner.html からの相対パスで -->
<script type="text/javascript" src="lib/jasmine-2.0.0/jasmine.js"></script>
<script type="text/javascript" src="lib/jasmine-2.0.0/jasmine-html.js"></script>
<script type="text/javascript" src="lib/jasmine-2.0.0/boot.js"></script>
<script type="text/javascript" src="lib/mocking.js"></script>
<!-- これを追加で読み込む -->
<script type="text/javascript" src="../lib/jquery-2.0.1.min.js"></script>
<script type="text/javascript" src="lib/jasmine-jquery.js"></script>
そして下記の HTML を spec/fixtures/hoge_fixture.html
のようなファイルに保存しておきます。
<div id="hoge"></div>
そしてテストを書きます。
describe('dom fixture', function() {
var element;
beforeEach(function() {
// フィクスチャが配置されているディレクトリを指定
jasmine.getFixtures().fixturesPath = 'fixtures';
// フィクスチャを読み込む
loadFixture('hoge_fixture.html');
// テスト対象のメソッドを呼び出す
fuga('dom fixture test');
element = document.getElementById('hoge');
});
it('should have a text', function() {
expect(element.innerText).toBe('dom fixture test');
});
});
ここまで準備ができたら SpecRunner.html
を開いてテストを実行します。ちなみに loadFixtures
は Ajax を使ってフィクスチャのファイルを読み込もうとしますが、Chrome を使っている場合、デフォルトではローカルファイル (SpecRunner.html
) からローカルファイル (fixtures/hoge_fixtures.html
) を Ajax で読むことができません。これを可能にするためには --allow-file-access-from-files
オプションをつけて Chrome を起動する必要があります。
- 立ち上げている Chrome があれば一度終了させる
--allow-file-access-from-files
オプションをつけて Google Chrome を起動する- Windows 7 の場合
C:\Users\[UserName]\AppData\Local\Google\Chrome[ SxS]\Application\chrome.exe --allow-file-access-from-files
- MacOSX の場合
open /Applications/Google\ Chrome.app --args --allow-file-access-from-files
- Windows 7 の場合
これで起動した Google Chrome で SpecRunner.html
を開くとテストを実行できます。
長くなってきたので続きはこちら。