ぶれすとつーる

だいたいjavascript

Nodeでプライベートな(exportsされてない)メソッドのテスト

だいたいこの記事のまんまですが大雑把な訳記事だと思ってください。

こんなファイル(app.js)があったとする。

//app.js
exports.testableMethod = function () {
  complicatedMethod(untestableMethod);
};

var untestableMethod = function (a, b) {
  return (a + b);
};

testableMethodはexportsされてるのでテストできますね(まぁ参照エラーでるけど)
こういう時untestableMethodのテストって結構至難の業でexportsされてるメソッド経由でテストしてたり、テスト用に

var local
  ;

exports.test = local = {};
local.privateMethodA = fucntion () {};

とかしてきってたんだけど、がんばればこういう規約的な回避方法以外にも道があるみたい。


Nodeにはvmモジュールがありますよね

このモジュールのAPI読んでくとcontextを自由に渡してコードを評価するやつがある。

vm.runInContext(code, context, [filename])#
vm.runInContext は code をコンパイルして、 context をコンテキストとして実行し、その結果を返します。 (V8 の) コンテキストは組み込みのオブジェクトと関数と共に、 グローバルオブジェクトを含みます。 実行されるコードはローカルスコープにアクセスせず、 context が code にとってのグローバルオブジェクトとして使われます。 filename はオプションで、スタックトレースでのみ使用されます。

つまりテスト対象のファイルのcodeをfsから抽出してvm.runInContextにセットしてcontextを適当につくってその参照を手元に残しながら第2引数に渡せば手元にのこったcontextの参照を使って局所変数テストできるんだぜって話です。

ここでいうcontextは変数オブジェクト(アクティベーションオブジェクト)だと思ってください。

そしてさっきのサイトにのってるその辺やってくれるコード

// module-loader.js
var vm = require('vm');
var fs = require('fs');
var path = require('path');

/**
 * Helper for unit testing:
 * - load module with mocked dependencies
 * - allow accessing private state of the module
 *
 * @param {string} filePath Absolute path to module (file to load)
 * @param {Object=} mocks Hash of mocked dependencies
 */
exports.loadModule = function(filePath, mocks) {
  mocks = mocks || {};

  // this is necessary to allow relative path modules within loaded file
  // i.e. requiring ./some inside file /a/b.js needs to be resolved to /a/some
  var resolveModule = function(module) {
    if (module.charAt(0) !== '.') return module;
    return path.resolve(path.dirname(filePath), module);
  };

  var exports = {};
  var context = {
    require: function(name) {
      return mocks[name] || require(resolveModule(name));
    },
    console: console,
    exports: exports,
    module: {
      exports: exports
    }
  };

  vm.runInNewContext(fs.readFileSync(filePath), context);
  return context;
};

を使えば最初あげたコードがこんな感じでテストできます(mocha使ったテスト)。

// test-app.js
var assert = require('assert')
  , loadModule = require('./module-loader').loadModule
  ;

describe('#untestableMethod', function () {
  var appContext = loadModule('./app.js')
    ;

  it('引数を足した結果を返すこと', function () {
    var actual = appContext.untestableMethod(1, 2)
      ;

    assert.equal(actual, 3);
  });
});


あとはテストするだけ


$ mocha test-app.js --reporter spec
#untestableMethod
引数を足した結果を返すこと

1 test complete (2 ms)


これでがんがんexportsされてない関数テストできますね。

幸せ度高いのでvojtajina氏に感謝。