如何访问和测试 node.js 模块中的内部(非导出)函数?

Posted

技术标签:

【中文标题】如何访问和测试 node.js 模块中的内部(非导出)函数?【英文标题】:How to access and test an internal (non-exports) function in a node.js module? 【发布时间】:2013-01-30 05:14:29 【问题描述】:

我正在尝试弄清楚如何在 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 函数进行单元测试而不实际导出它,因为它并不打算公开?

【问题讨论】:

也许只是在特定环境中公开要测试的功能?我不知道这里的标准程序。 没有导出是有原因的。只测试公共接口,任何私有的都会一路测试。 【参考方案1】:

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

【讨论】:

【参考方案2】:

诀窍是将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;

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

【讨论】:

这似乎是一个 hack,如果 NODE_ENV 阻塞,真的没有办法测试内部(非导出)函数吗? 这太恶心了。这不是解决此问题的最佳方法。 我根本不认为这是一个 hack——如果你能够修改库,那么这是一种完全没有任何第三方依赖关系的导出未导出标识符的完整方法。归根结底,两者都允许您访问您不应该正常访问的东西 - 所以按照这种逻辑,这整个想法是一个黑客和讨厌的。【参考方案3】:

编辑:

使用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;;

节点模块的全局 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 访问非导出函数吗?【参考方案4】:

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

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

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

function myInternalFn() 



function myExportableFn() 
    myInternalFn();   


exports.myExportableFn = myExportableFn;

如果我们现在想要测试 spy 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;

现在您可以在任何将 myInternalFn 用作 testable.myInternalFn 的地方测试、监视和模拟它,并且在生产中它不导出

【讨论】:

【参考方案5】:

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


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

【讨论】:

这绝对应该是最佳答案。它不需要使用特定于 NODE_ENV 的导出重写所有现有模块,也不涉及将模块作为文本读取。 很好的解决方案。有适合 Babel 类型的人的工作版本吗? 将 rewire 与 jest 和 ts-jest(打字稿)一起使用,我收到以下错误:Cannot find module '../../package' from 'node.js'。你见过这个吗? Rewire 与 jest 存在兼容性问题。 Jest 不会在覆盖报告中考虑从 rewire 调用的函数。这在某种程度上违背了目的。 是的,这是一个解决方案。唯一的问题是 Jest 的测试覆盖率报告中没有考虑重新连接的模块。【参考方案6】:

与 Jasmine 合作,我尝试在 solution proposed by 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);

【讨论】:

【参考方案7】:

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

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

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

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

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

【讨论】:

肯定有创意的解决方案【参考方案8】:

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

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

【讨论】:

【参考方案9】:

eval 本身并不真正起作用(它只适用于***函数或 var 声明),您无法捕获已声明的***变量使用 let 或 const 进入当前上下文 使用 eval 但是,使用 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 运行您的目标模块以运行其测试。

【讨论】:

【参考方案10】:

我一直在使用不同的方法,没有任何依赖关系: 使用我要测试的所有本地函数进行 __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

【讨论】:

以上是关于如何访问和测试 node.js 模块中的内部(非导出)函数?的主要内容,如果未能解决你的问题,请参考以下文章

Node.js学习笔记-模块化开发

Node.js 和 Typescript,如何动态访问导入的模块

02 node.js模块化开发

如何在Node.js中对使用promises和事件发射器的函数进行单元测试?

您如何安装和运行 Mocha,Node.js 测试模块?安装后获取“mocha:找不到命令”

如何自动更新所有 Node.js 模块?