Node.jsのテストで特定の関数をモックと置き換える

テストコードを書くとき、既存の関数を置き換えて何もさせたくないようにしたい場合が多々ある。

例えば、console.logを上書きして何も出力させないようにする、など。

JavaScriptなら関数の差し替え自体は簡単だ。モジュールのプロパティに別の関数をぶっこめば良い。

[nodeunit](https://www.npmjs.com/package/nodeunit)を使ってテストケースを書く場合、

```js
// nodeunitのテストケース

exports.setUp = function(done){
    // console.logを何もしない関数と置き換える
    console.log = function noop() {
    }
    done();
};
```

となる。


ただ、差し替えた後、やっぱり戻したくなる場合も多々ある。


```js
// nodeunitのテストケース

var log;
exports.setUp = function(done){
    // console.logを何もしない関数と置き換える
    log = console.log;
    console.log = function noop() {
    }
    done();
};
exports.tearDown = function(done){
    // console.logを戻す
    console.log = log;
    done();
};
```

こうなると若干めんどくさい。

別の変数に退避して戻すだけなのだが、数が増えてくるとやっかいだ。
ちゃんと戻したかどうかいちいち意識しないといけない。

どうせなら、戻すとき時はまとめて勝手にやってくれるようにしたい。


モックの差し込みを復元する方法を考えてみる
------

手取り早いのは差し込みの時点でどっかに覚えておいて、後でまとめて戻す、という関数を作ることだ。

雰囲気としては、

```js

// Disable console.log before test started.
exports.setUp = function (done) {
    function mockLog() {
    }

    // Inject `mockLog` as `console.log`.
    injectmock(console, 'log', mockLog);

    done();
};

// Disable console.log before test done.
exports.tearDown = function (done) {
    // Restore all injected.
    injectmock.restoreAll();

    console.log('Now I am back!');

    done();
};
```

的な。`injectmock.restoreAll()`で何も考えずに全部戻してほしい。



モックの差し込み・復元を管理する関数を実装する
------

それ用のクラスを作って、

+ 対象のモジュール
+ 置換された関数
+ プロパティ名

を管理するようにすれば良い。


```js

/**
 * Inject mock functions to node modules.
 * @memberof injectmock
 * @inner
 * @constructor Injector
 */
function Injector() {
    var s = this;
    s.injections = [];
}
Injector.prototype = {
    /**
     * Inject a mock to a module.
     * @param {object} module - Module to injected.
     * @param {string} key - Injection key.
     * @param {*} value - Value to inject.
     * @returns {*} - Returns self.
     */
    inject: function (module, key, value) {
        var s = this;
        s.injections.unshift({
            module: module,
            key: key,
            delete: !module.hasOwnProperty(key),
            origin: module[key],
            value: value
        });
        module[key] = value;
        return s;
    },
    /**
     * Restore injected.
     * @param {object} module - Module to injected.
     * @param {string} key - Injection key.
     * @returns {*} - Returns self.
     */
    restore: function (module, key) {
        var s = this;
        var index = s._indexOfInjection(module, key);
        var injection = s.injections[index];
        if (injection) {
            if (injection.delete) {
                delete module[key];
            } else {
                module[key] = injection.origin;
            }
            s.injections.splice(index, 1);
        } else {
            // Do nothing.
        }
        return s;
    },
    /**
     * Restore all injected.
     * @returns {*} - Returns self.
     */
    restoreAll: function () {
        var s = this;
        while (s.injections.length) {
            var injection = s.injections[0];
            s.restore(injection.module, injection.key);
        }
        return s;
    },
    _indexOfInjection: function (module, key) {
        var s = this;
        for (var i = 0, len = s.injections.length; i < len; i++) {
            var injection = s.injections[i],
                hit = (injection.key === key) && (injection.module === module);
            if (hit) {
                return i;
            }
        }
        return -1;
    }
};
```

後はこいつを呼び出し安いようにちょろっと加工する。

```js
function context() {
    var injector = new Injector();
    var inject = injector.inject.bind(injector);
    inject.restore = injector.restore.bind(injector);
    inject.restoreAll = injector.restoreAll.bind(injector);
    return inject;
}

var injectmock = context();
module.exports = injectmock;
```

ポイントは

```
var inject = injector.inject.bind(injector);
```

で、インスタンスのメソッドにインスタンス自身をバインドすることで、
内部状態を持つ関数をインスタンスを意識しないで使えるようになる。



npmパッケージにまとめる
----

後は例のごとく整理してテストを書いてnpmパッケージにしてみた。

+ [injectmock](https://www.npmjs.com/package/injectmock)


```bash
npm install injectmock --save-dev
```

でインストールして、

```js

var injectmock = require('injectmock');
 
// Disable console.log before test started. 
exports.setUp = function (done) {
    function mockLog() {
    }
 
    // Inject `mockLog` as `console.log`. 
    injectmock(console, 'log', mockLog);
 
    done();
};
 
// Disable console.log before test done. 
exports.tearDown = function (done) {
    // Restore all injected. 
    injectmock.restoreAll();
 
    console.log('Now I am back!');
 
    done();
};
```

で使えるようにした。




Links
-----

+ [nodeunit](https://www.npmjs.com/package/nodeunit)
+ [injectmock](https://www.npmjs.com/package/injectmock)