vsts-task-libで使われている mockeryの挙動が謎だったので、ハローワールドレベルから試して見た。とっても、しょーもないことでクソハマったが、それも含めて書いておく。
mockery の mock 方式
ドキュメントを読んでいると、node のモックは難しいらしく、普通のフレームワークとは違う方式をとっている様子。基本的な話をすると、require(...)
が呼ばれたタイミングで、require
の先のモジュールをモックにごっそり入れ替えるという方式みたい。
ちなみに、そういう方式なので、
test.js -> index.js -> special.js
といった感じになっているときに、require('index.js')
にも、require('special.js')
にもモックをかけられる。つまり、モジュールを読んだ先のモジュールもモックができる。
コードサンプル
コードを書いてみよう。vsts-task-lib もそうなので、あえて、typescript で記述する。
test.ts
var expect = require('chai').expect;
var Index = "../index";
var mockery = require('mockery');
var mock_index_module = {
request: function() {
return "I'm mocking";
}
}
var mock_special_module = {
get_rest_api: function() {
return 'mock special rest api';
}
}
describe('Array', function() {
describe('mockery testing', function () {
it('is no mock testing', function() {
var index = require(Index);
expect(index.request()).to.equal('real http request result');
});
it('mocks index', function() {
mockery.registerAllowable(Index);
mockery.registerMock(Index, mock_index_module);
mockery.enable({ useCleanCache: true});
var index = require(Index);
expect(index.request()).to.equal("I'm mocking");
// expect(index.other_request()).to.equal('real other response body'); // error
mockery.disable();
mockery.deregisterAll();
})
it('mocks special', function() {
mockery.registerAllowable(Index);
mockery.registerMock("./special", mock_special_module);
mockery.enable({ useCleanCache: true}); // 要注意。指定しないと動かない。useCleanChache と間違えてた。
var index = require(Index);
expect(index.request()).to.equal("mock special rest api");
mockery.disable();
mockery.deregisterAll();
})
});
});
index.ts
import special = require('./special');
function request() {
return special.get_rest_api();
}
function other_request() {
return special.get_other_api();
}
exports.request = request;
exports.other_request = other_request;
special.ts
export function get_rest_api() {
return 'real http request result';
};
export function get_other_api() {
return 'real other response body';
}
ポイントはいくつかあって、mock をかけたいときは、mock を登録して、有効化する必要がある。
使っているメソッドを解説して行きたい。サンプルでは、一段めのモジュール、二段目のモジュールに両方モックをかけている。
mockery.registerAllowable('./some_mock_package')
registerAllowable
は、モック対象のパッケージとその配下のパッケージに関してモックをかけるぞと宣言するもの。実際は、その配下のrequire
でモックをしなかったらワーニングが出てしまうのでそれを防ぐために記述する。同時に書こうと思ったら
mockery.registerAllowables(['async', 'path', 'util'])
という感じでもかける。終わったら deregisterAllowable
をしておく。
mockery.registerAllowable('./my-source-under-test', true)
という風にもかけるらしい。node はデフォルトでは、モジュールを1回しか読まない。同じパッケージに対して違うMockをかけたい時もあるだろう。そういう時は、unhooking
というらしいが、上記のように、true
を指定することで、nodeのモジュールが一旦ディレジスターされる。
mockery.registerMock("./special", mock_special_module)
これは、Mock するモジュールを指定する。./special
の部分は、ワイルドカードとか、かしこい機能がないので、require
で呼ばれる時と全く同じ記述が必要らしい。mock_special_module
はモック対象を記述する。こんな感じ。
var mock_index_module = {
request: function() {
return "I'm mocking";
}
}
mockery.enable({ useCleanCache: true})
Mock を有効化している。ここの部分で気をつけるのがキャッシュだ。ここでは、useCleanCache
にtrue
をセットしている。私もキャッシュの恐ろしさを思い知った(クソハマった)node モジュールのexport は常にキャッシュされる。これは、モックをものすごく難しくしている。mockery
は、モジュールのキャッシュをクリアするメカニズムを持っている。このオプションを指定すると、以前のキャッシュうは消されるので良い。
私がハマったのは、useCleanCache
をスペルミスしていて、気づかず、エラーも特にでずなぜか Mockがかからないという状態になった。他にも、mockery.resetCache()
なんてメソッドもある様子。
var index = require('./some_mock_package')
あとは、Mock 対象のモジュールを読み込めばいい。実際にモックがかかるのが、そのモジュールの配下としても、一段目のモジュールを指定する。ここのモジュールはregisterAllowable
と同じになるはず。mockeryを有効化して、require
をした瞬間から、この配下のrequire
に登録したmockがあれば、差し替えられる。
ちなみに、Mockのモジュールは、差し替え対象のモジュールが、function A
, function B
を持っていたとすると、両方の記述が必要になる。(実際は、function A
だけモックしたいケースもあるだろうが、そのケースは、function B` から本物にデリゲートするのが良さげかも。
終わりに
Mockery の動作イメージは掴めたと思う。ただ、issue
を見ていると、多くの人が、mock
がかからない問題をレポートしている。ただ、私のように、issue
を見ると、初期化のタイミングのミスだったり、そういう、普通のMockフレームワークと使い勝手が違うところに起因するようだ。キャッシュが難しいとよくよくわかったのであった。
ちなみに、サンプルソースは、上げておいた。
本当はここからもっと実験して見たいところだが、先ほどのしょうもない問題で引っかかって今2時なので寝ることにする。無念。しょーもないものほどハマってしまう。キャッシュは恐ろしいということで。