219

我试图弄清楚如何在 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不实际导出函数的情况下对函数进行单元测试,因为它不是要公开的?

4

10 回答 10

285

重新布线模块绝对是答案。

这是我用于访问未导出函数并使用 Mocha 对其进行测试的代码。

应用程序.js:

function logMongoError(){
  console.error('MongoDB Connection Error. Please make sure that MongoDB is running.');
}

测试.js:

var rewire = require('rewire');
var chai = require('chai');
var should = chai.should();


var app = rewire('../application/application.js');


var 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();
  });
});
于 2015-06-12T02:02:52.103 回答
17

诀窍是将NODE_ENV环境变量设置为类似test然后有条件地导出它。

假设你没有全局安装 mocha,你可以在你的 app 目录的根目录下有一个 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;

另一个答案建议使用 vm 模块来评估文件,但这不起作用并引发错误,指出未定义导出。

于 2013-10-13T10:15:05.213 回答
7

编辑:

使用加载模块vm可能会导致意外行为(例如,instanceof运算符不再使用在此类模块中创建的对象,因为全局原型与在正常加载的模块中使用的原型不同require)。我不再使用下面的技术,而是使用rewire模块。它工作得很好。这是我的原始答案:

详细说明 srosh 的答案...

感觉有点 hacky,但我写了一个简单的“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;};

节点模块的 gobalmodule对象中包含更多的东西,可能还需要进入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));
  });
});
于 2014-02-27T05:08:24.830 回答
2

与 Jasmine 合作,我尝试更深入地研究Anthony Mayfield 提出的基于rewire的解决方案。

我实现了以下功能注意:尚未彻底测试,只是作为一种可能的策略共享)

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);
于 2016-10-02T15:46:32.697 回答
0

您可以使用vm模块创建一个新上下文并评估其中的 js 文件,有点像 repl 所做的。然后您可以访问它声明的所有内容。

于 2013-02-14T19:59:10.877 回答
0

我找到了一种非常简单的方法,可以让您从测试中测试、监视和模拟这些内部函数:

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

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现在,您可以在任何使用它的地方进行测试、监视和模拟,testable.myInternalFn并且在生产中它不会被导出

于 2014-10-23T07:03:01.153 回答
0

这不是推荐的做法,但如果您不能rewire按照@Antoine 的建议使用,您可以随时读取文件并使用eval().

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

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

windowJS 文件会在没有任何require(...)and语句的情况下设置很多全局变量(无论如何,module.exports没有像 Webpack 或 Browserify 这样的模块捆绑器可用于删除这些语句)。

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

于 2018-07-25T12:32:03.363 回答
0

本质上,您需要将源上下文与测试用例合并 - 一种方法是使用包装测试的小型辅助函数。

演示.js

const internalVar = 1;

demo.test.js

const importing = (sourceFile, tests) => eval(`${require('fs').readFileSync(sourceFile)};(${String(tests)})();`);


importing('./demo.js', () => {
    it('should have context access', () => {
        expect(internalVar).toBe(1);
    });
});

于 2020-11-21T11:17:10.067 回答
0

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

...尽管“导入”模块中的声明或分配可能与虚拟机启动时已在当前上下文中声明/定义的任何内容发生冲突,如果它们共享相同的名称。

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

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

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
}

现在直接运行你的目标模块node MODULEPATH来运行它的测试。

于 2021-03-05T10:18:08.510 回答
0

我一直在使用一种不同的方法,没有任何依赖关系: 使用我要测试的所有本地函数进行 __testing 导出,该值取决于 NODE_ENV,因此只能在测试中访问:

// file.ts
const localFunction = () => console.log('do something');
const localFunciton2 = () => console.log('do something else');
export const exportedFunction = () => {
    localFunction();
    localFunciton2();
}
export const __testing = (process.env.NODE_ENV === 'test') ? {
    localFunction, localFunction2
} : void 0;

// file.test.ts
import { __testing, exportedFunction } from './file,ts'
const { localFunction, localFunction2 } = __testing!;
// Now you can test local functions
于 2022-01-07T08:59:09.647 回答