Nodejsの内部(つまり、エクスポートされていない)関数をテストする方法を理解しようとしています(できればmochaまたはjasmineを使用)。そして、私にはわからない!
そのようなモジュールがあるとしましょう:
function exported(i) {
return notExported(i) + 1;
}
function notExported(i) {
return i*2;
}
exports.exported = exported;
そして、次のテスト(mocha):
var assert = require('assert'),
test = require('../modules/core/test');
describe('test', function(){
describe('#exported(i)', function(){
it('should return (i*2)+1 for any given i', function(){
assert.equal(3, test.exported(1));
assert.equal(5, test.exported(2));
});
});
});
notExported
関数は、公開することを意図していないため、実際にエクスポートせずに単体テストする方法はありますか?
rewire モジュールが間違いなく答えです。
以下は、エクスポートされていない関数にアクセスし、Mochaを使用してテストするための私のコードです。
application.js:
function logMongoError(){
console.error('MongoDB Connection Error. Please make sure that MongoDB is running.');
}
test.js:
var rewire = require('rewire');
var chai = require('chai');
var should = chai.should();
var app = rewire('../application/application.js');
logError = app.__get__('logMongoError');
describe('Application module', function() {
it('should output the correct error', function(done) {
logError().should.equal('MongoDB Connection Error. Please make sure that MongoDB is running.');
done();
});
});
トリックは、NODE_ENV
環境変数をtest
のようなものに設定してから、条件付きでエクスポートすることです。
Mochaをグローバルにインストールしていない場合、アプリディレクトリのルートに次を含むMakefileを作成できます。
REPORTER = dot
test:
@NODE_ENV=test ./node_modules/.bin/mocha \
--recursive --reporter $(REPORTER) --ui bbd
.PHONY: test
このmakeファイルは、mochaを実行する前にNODE_ENVをセットアップします。その後、コマンドラインでmake test
を使用してmochaテストを実行できます。
これで、通常はmochaテストの実行中にのみエクスポートされない関数を条件付きでエクスポートできます。
function exported(i) {
return notExported(i) + 1;
}
function notExported(i) {
return i*2;
}
if (process.env.NODE_ENV === "test") {
exports.notExported = notExported;
}
exports.exported = exported;
もう1つの答えは、vmモジュールを使用してファイルを評価することを提案しましたが、これは機能せず、エクスポートが定義されていないことを示すエラーをスローします。
編集:
vm
を使用してモジュールをロードすると、予期しない動作が発生する可能性があります(例:instanceof
演算子は、グローバルプロトタイプがrequire
で通常ロードされるモジュールで使用されるものと異なるため、そのようなモジュールで作成されたオブジェクトでは動作しなくなります)。以下の手法は使用せず、代わりに rewire モジュールを使用します。それは見事に機能します。私の元の答えは次のとおりです。
Sroshの答えを詳しく説明しています...
少しハックが感じられますが、アプリケーションモジュールで条件付きエクスポートを行わずに、必要な処理を実行できるシンプルな「test_utils.js」モジュールを作成しました。
var Script = require('vm').Script,
fs = require('fs'),
path = require('path'),
mod = require('module');
exports.expose = function(filePath) {
filePath = path.resolve(__dirname, filePath);
var src = fs.readFileSync(filePath, 'utf8');
var context = {
parent: module.parent, paths: module.paths,
console: console, exports: {}};
context.module = context;
context.require = function (file){
return mod.prototype.require.call(context, file);};
(new Script(src)).runInNewContext(context);
return context;};
ノードモジュールのgobal module
オブジェクトには、上記のcontext
オブジェクトにも追加する必要があるかもしれないものがいくつかありますが、これが動作するために必要な最小限のセットです。
Mocha BDDを使用した例を次に示します。
var util = require('./test_utils.js'),
assert = require('assert');
var appModule = util.expose('/path/to/module/modName.js');
describe('appModule', function(){
it('should test notExposed', function(){
assert.equal(6, appModule.notExported(3));
});
});
ジャスミンと協力して、私は rewire に基づいて Anthony Mayfieldによって提案された解決策 をさらに深めようとしました。
次の関数を実装しました(Caution:まだ徹底的にテストされておらず、可能性のある戦略として共有されています) :
function spyOnRewired() {
const SPY_OBJECT = "rewired"; // choose preferred name for holder object
var wiredModule = arguments[0];
var mockField = arguments[1];
wiredModule[SPY_OBJECT] = wiredModule[SPY_OBJECT] || {};
if (wiredModule[SPY_OBJECT][mockField]) // if it was already spied on...
// ...reset to the value reverted by jasmine
wiredModule.__set__(mockField, wiredModule[SPY_OBJECT][mockField]);
else
wiredModule[SPY_OBJECT][mockField] = wiredModule.__get__(mockField);
if (arguments.length == 2) { // top level function
var returnedSpy = spyOn(wiredModule[SPY_OBJECT], mockField);
wiredModule.__set__(mockField, wiredModule[SPY_OBJECT][mockField]);
return returnedSpy;
} else if (arguments.length == 3) { // method
var wiredMethod = arguments[2];
return spyOn(wiredModule[SPY_OBJECT][mockField], wiredMethod);
}
}
このような関数を使用すると、次のように、エクスポートされていないオブジェクトとエクスポートされていない最上位関数の両方のメソッドをスパイできます。
var dbLoader = require("rewire")("../lib/db-loader");
// Example: rewired module dbLoader
// It has non-exported, top level object 'fs' and function 'message'
spyOnRewired(dbLoader, "fs", "readFileSync").and.returnValue(FULL_POST_TEXT); // method
spyOnRewired(dbLoader, "message"); // top level function
次に、次のような期待値を設定できます。
expect(dbLoader.rewired.fs.readFileSync).toHaveBeenCalled();
expect(dbLoader.rewired.message).toHaveBeenCalledWith(POST_DESCRIPTION);
テスト内からこれらのinternal関数をテスト、スパイ、およびモックできる非常に簡単な方法を見つけました。
次のようなノードモジュールがあるとします。
mymodule.js:
------------
"use strict";
function myInternalFn() {
}
function myExportableFn() {
myInternalFn();
}
exports.myExportableFn = myExportableFn;
テストする場合and spy and mock myInternalFn
実動環境ではエクスポートせずにこのようにファイルを改善する必要があります:
my_modified_module.js:
----------------------
"use strict";
var testable; // <-- this is new
function myInternalFn() {
}
function myExportableFn() {
testable.myInternalFn(); // <-- this has changed
}
exports.myExportableFn = myExportableFn;
// the following part is new
if( typeof jasmine !== "undefined" ) {
testable = exports;
} else {
testable = {};
}
testable.myInternalFn = myInternalFn;
これで、testable.myInternalFn
として使用するすべての場所でmyInternalFn
をテスト、スパイ、およびモックできます。実稼働環境ではエクスポートされないです。
これは推奨される方法ではありませんが、@ Antoineが提案するrewire
を使用できない場合は、いつでもファイルを読み取ってeval()
を使用できます。
var fs = require('fs');
const JsFileString = fs.readFileSync(fileAbsolutePath, 'utf-8');
eval(JsFileString);
これは、レガシーシステムのクライアント側JSファイルを単体テストするときに便利です。
JSファイルは、require(...)
およびmodule.exports
ステートメントなしでwindow
の下に多くのグローバル変数を設定します(これらのステートメントを削除するためのWebpackやBrowserifyなどのモジュールバンドルはありませんでした)。
これにより、コードベース全体をリファクタリングするのではなく、クライアント側のJSに単体テストを統合できました。
vm moduleを使用して新しいコンテキストを作成し、その中のjsファイルを評価できます。これはreplと同様です。その後、宣言するすべてにアクセスできます。