こんにちは @ryu1kn です。これは Visual Studio Code Advent Calendar 2016 の2日目の記事です。
Visual Studio Code (以下 VS Code) はバージョンが1に届いた頃からメインで使うようになりました。以降いくつかエクステンションを書いたのですが、今回はそれらのエクステンション開発時に必要だったユニットテストの環境準備について書きます。
TL;DR
- エクステンション生成時についてくるテストの実行環境は VS Code の全 API が使えるインテグレーションテスト用のものであり、起動が遅く TDD でのユニットテストには向かない
- コマンドラインから手早くユニットテストを実行できるようにしておくと快適に TDD できる
- VS Code の API を提供する vscode モジュールは、エディタ上で実行する時とコマンドラインから実行する時とで違うものになる
- いつも依存性注入 (DI) をしていればユニットテストを書く上でこのことが問題になることはないが、そうしたくない(?)時は...
プロジェクト生成時にはインテグレーションテストのサンプルがついてくる
Testing Your Extension にある通りですが、 Yeoman generator で Visual Studio Code のエクステンションの雛形を生成すると、サンプルのテストもついてきます。
プロジェクトの .vscode/launch.json
には Launch Tests というエントリが予め用意されているので、デバッグビューを開いて Launch Tests を選択・実行すれば、エディタのウィンドウが新しく開かれ、その中でサンプルテストが実行されます。
サンプルテスト内にある以下のコメントの通り、 vscode
をインポートすればエディタの API が使えるようになります。
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
var vscode = require('vscode');
API を介して実際にエディタを操作しながらテストを行うことが可能で、 VS Code ではこうしたテストをインテグレーションテストと呼んでいます。インテグレーションテストは新しいエディタのインスタンスを起動した上でテストを実行するため、テスト開始までに時間がかかります。
一方、エクステンションを開発する際に書くユニットテストは実際にエディタを操作する必要がないため、エディタから独立してコマンドラインなどから実行することが可能です。テスト開始に時間がかかることもないので、テスト・実装・リファクタリングの TDD サイクルをリズムよく回すことができます。
ユニットテストの実行環境を用意する
ここではユニットテストの実行に mocha を使います。特別な使い方はしていません。
npm test
でテストを実行できるように package.json
の scripts
に test
を登録しています。開発中は頻繁にテストを実行するので watch
オプションをつけた test-mode
というコマンドもついでに登録しています。
-
package.json
{ "scripts": { "test": "mocha --opts ./test/cli-test-mocha.opts", "test-mode": "mocha --opts ./test/cli-test-mocha.opts --watch", ... }, ... }
mocha
への設定は少し長くなることもあるので opts
ファイルに分けています。
-
test/cli-test-mocha.opts
--recursive --ui tdd test
これでコマンドラインからテストが実行できるようになりました。
vscode モジュールは Visual Studio Code 上で実行するかどうかで別物になる
Yeoman generator で生成したエクステンションの package.json には予め vscode
が devDependencies
の1つとして入っています。この vscode
モジュールはインテグレーションテストの実行を助けるものであり、VS Code の API を含んではいません。つまり require('vscode')
した時に得られるものは、 VS Code の上で実行する時とそれ以外(コマンドラインなど)から実行する時とで違います!
それでも、エディタの API を使うコンポーネントには vscode
を DI するように実装していれば、このことがユニットテストで問題になることはありません。ユニットテストでコンポーネントに渡す vscode
は require('vscode')
で取得したものではなく、テストに応じて必要な分だけ定義したモックだからです。
それでも例えば次のように「 vscode
の提供する定数(実際は Enum )を参照しているのみだから」と DI していないコンポーネントでは問題になります。
const OverviewRulerLane = require('vscode').OverviewRulerLane;
class Foo {
// OverviewRulerLane を使う何らかのコード
}
このコードは Visual Studio Code の上で実行されれば何も問題ありませんが、ユニットテストから実行されると require('vscode')
で例外が発生します。
この問題は例えば次のような方法で回避できるでしょう。
- (そもそもテスト可能にすることが DI の大きな利点なので)モジュールの定数の参照のみであっても DI する
- (そもそも
require('vscode')
が実行環境によって違うものになるのが問題なので)devDependencies
にあるvscode
の代わりに自分で用意したモックを読み込ませる
2つ目のアプローチは nodejs のモジュール読み込みの仕組みに手を入れることになるので避けたい感いっぱいですが、補足しておきます。
テストで Foo をロードする時には require
の代わりに proxyquire のようなモジュールを使い、 vscode
をモックオブジェクトに置き換えてしまう方法が取れます。
const proxyquire = require('proxyquire');
const mockVscode = {
OverviewRulerLane: {Center: 'OVERVIEW_RULER_LANE_CENTER'}
};
const Foo = proxyquire('../lib/foo', {vscode: mockVscode});
suite('Foo', () => {
test('something', () => {
...
もしくは
- エディタ上で実行する時の定数を使ってテストしてみたい
-
proxyquire
のようなパッケージを使いたくない
のような事情があれば自分でモックを読み込むコードを用意できます。その場合はモックへの置き換えはテストファイルの外に出し、コマンドラインからの実行時のみ置き換えればいいでしょう。
-
test/stub-vscode.js
// HACK: As vscode is not available if tests are not running on VSCode's extensionHostProcess, // return mock vscode module when `require`d const mockVscode = { OverviewRulerLane: {Center: 2} }; const moduleProto = Object.getPrototypeOf(module); moduleProto.require = new Proxy(moduleProto.require, { apply: (require, that, args) => args[0] === 'vscode' ? mockVscode : Reflect.apply(require, that, args) });
-
test/cli-test-mocha.opts
--recursive --ui tdd --require test/stub-vscode.js test
上の例で stub-vscode.js で設定する OverviewRulerLane.Center
の値は適当な値ではなく、エディタ実行時に OverviewRulerLane.Center
が持つ値にする必要があります(そうでないと VS Code の上でユニットテストを実行した時にこけてしまいます)
最後に
このポストに書いたことは TextMarker というエクステンションを作った時に試してみたことなので、もしよければリポジトリを見てみて下さい。