Posted at

ユニットテストの実行環境を用意して Visual Studio Code のエクステンションを快適に TDD する

More than 1 year has passed since last update.

こんにちは @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.jsonscriptstest を登録しています。開発中は頻繁にテストを実行するので 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 には予め vscodedevDependencies の1つとして入っています。この vscode モジュールはインテグレーションテストの実行を助けるものであり、VS Code の API を含んではいません。つまり require('vscode') した時に得られるものは、 VS Code の上で実行する時とそれ以外(コマンドラインなど)から実行する時とで違います!

それでも、エディタの API を使うコンポーネントには vscode を DI するように実装していれば、このことがユニットテストで問題になることはありません。ユニットテストでコンポーネントに渡す vscoderequire('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 というエクステンションを作った時に試してみたことなので、もしよければリポジトリを見てみて下さい。