我试图弄清楚如何在nodejs中测试内部(即不导出)函数(最好使用mocha或jasmine)。我也不知道!

假设我有一个这样的模块

function exported(i) {
   return notExported(i) + 1;
}

function notExported(i) {
   return i*2;
}

exports.exported = exported;

以及以下测试(摩卡):

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函数进行单元测试,而不实际导出它,因为它不打算公开?


当前回答

我发现了一种非常简单的方法,可以让你在测试中测试、监视和模拟这些内部函数:

假设我们有一个这样的节点模块:

mymodule.js:
------------
"use strict";

function myInternalFn() {

}

function myExportableFn() {
    myInternalFn();   
}

exports.myExportableFn = myExportableFn;

如果我们现在想测试并监视和模拟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;

现在你可以测试,间谍和模拟myInternalFn在任何地方,你使用它作为测试。myInternalFn和在生产中它是不导出的。

其他回答

我发现了一种非常简单的方法,可以让你在测试中测试、监视和模拟这些内部函数:

假设我们有一个这样的节点模块:

mymodule.js:
------------
"use strict";

function myInternalFn() {

}

function myExportableFn() {
    myInternalFn();   
}

exports.myExportableFn = myExportableFn;

如果我们现在想测试并监视和模拟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;

现在你可以测试,间谍和模拟myInternalFn在任何地方,你使用它作为测试。myInternalFn和在生产中它是不导出的。

Eval本身并不能工作(它只适用于顶级函数或var声明),你不能用Eval捕获用let或const声明的顶级变量到当前上下文中,但是,使用一个vm并在当前上下文中运行它将允许你在它执行后访问所有顶级变量…

eval("let local = 42;")
// local is undefined/undeclared here
const vm = require("vm")
vm.runInThisContext("let local = 42;");
// local is 42 here

...虽然"imported"模块中的声明或赋值可能在vm启动时与当前上下文中已经声明/定义的任何内容冲突,但前提是它们具有相同的名称。

这是一个一般的解决方案。这将为您导入的模块/单元添加少量不必要的代码,并且您的测试套件将不得不直接运行每个文件,以便以这种方式运行其单元测试。如果没有更多的代码,直接运行模块来做任何事情,而不是运行单元测试,这是不可能的。

在导入的模块中,检查文件是否是主模块,如果是,运行测试:

const local = {
  doMath() {return 2 + 2}
};

const local2 = 42;

if (require.main === module) {
  require("./test/tests-for-this-file.js")({local, local2});
} 

然后在导入目标模块的测试文件/模块中:

module.exports = function(localsObject) {
  // do tests with locals from target module
}

现在直接使用节点MODULEPATH运行目标模块以运行其测试。

与Jasmine合作,我试图深入研究Anthony Mayfield提出的基于重新布线的解决方案。

我实现了以下函数(注意:还没有完全测试,只是作为一个可能的策略分享):

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);

这是不推荐的实践,但如果您不能像@Antoine建议的那样使用rewire,则始终可以读取文件并使用eval()。

var fs = require('fs');
const JsFileString = fs.readFileSync(fileAbsolutePath, 'utf-8');
eval(JsFileString);

在为遗留系统进行客户端JS文件单元测试时,我发现这很有用。

JS文件会在window下设置很多全局变量,没有任何require(…)和模块。exports语句(没有像Webpack或Browserify这样的模块绑定器可以删除这些语句)。

这使得我们可以在客户端JS中集成单元测试,而不是重构整个代码库。

你可以使用vm模块创建一个新的上下文,并对其中的js文件进行eval,有点像repl。然后你就可以访问它声明的所有内容。