Mockery を試してみる

[vsts-task-lib](https://github.com/Microsoft/vsts-task-lib)で使われている [mockery](https://github.com/mfncooper/mockery)の挙動が謎だったので、ハローワールドレベルから試して見た。とっても、しょーもないことでクソハマったが、それも含めて書いておく。

# mockery の　mock 方式

ドキュメントを読んでいると、node のモックは難しいらしく、普通のフレームワークとは違う方式をとっている様子。基本的な話をすると、`require(...)` が呼ばれたタイミングで、`require` の先のモジュールをモックにごっそり入れ替えるという方式みたい。

ちなみに、そういう方式なので、

```
test.js -> index.js -> special.js
````

といった感じになっているときに、`require('index.js')` にも、`require('special.js')` にもモックをかけられる。つまり、モジュールを読んだ先のモジュールもモックができる。

# コードサンプル

コードを書いてみよう。vsts-task-lib もそうなので、あえて、typescript で記述する。

_test.ts_

```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_

```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 はデフォルトでは、モジュールを１回しか読まない。同じパッケージに対して違うMockをかけたい時もあるだろう。そういう時は、`unhooking` というらしいが、上記のように、`true` を指定することで、nodeのモジュールが一旦ディレジスターされる。

## mockery.registerMock("./special", mock_special_module)

これは、Mock するモジュールを指定する。`./special` の部分は、ワイルドカードとか、かしこい機能がないので、`require` で呼ばれる時と全く同じ記述が必要らしい。`mock_special_module` はモック対象を記述する。こんな感じ。

```ts
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フレームワークと使い勝手が違うところに起因するようだ。キャッシュが難しいとよくよくわかったのであった。

ちなみに、サンプルソースは、上げておいた。

* [TsuyoshiUshio/MockerySpike](https://github.com/TsuyoshiUshio/MockerySpike)

本当はここからもっと実験して見たいところだが、先ほどのしょうもない問題で引っかかって今２時なので寝ることにする。無念。しょーもないものほどハマってしまう。キャッシュは恐ろしいということで。