首页 文章

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

提问于
浏览
108

我试图弄清楚如何测试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 函数而不实际导出它,因为它不打算暴露?

7 回答

  • 1

    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();
      });
    });
    
  • 1

    诀窍是将 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模块来评估文件,但这不起作用并抛出错误,指出未定义导出 .

  • 0

    编辑:

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

    节点模块的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));
      });
    });
    
  • 14

    我发现了一种非常简单的方法,允许您在测试中测试,监视和模拟这些内部函数:

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

    mymodule.js:
    ------------
    "use strict";
    
    function myInternalFn() {
    
    }
    
    function myExportableFn() {
        myInternalFn();   
    }
    
    exports.myExportableFn = myExportableFn;
    

    如果我们现在要测试 and spy and mock myInternalFn while not exporting it in production ,我们必须像这样改进文件:

    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;
    

    现在你可以测试, Spy 和模拟 myInternalFn 你在哪里使用它作为 testable.myInternalFn 并且在 生产环境 中它是 not exported .

  • 7

    与Jasmine合作,我试图根据rewire深入了解solution proposed by 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);
    
  • 160

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

  • 0

    这不是推荐的做法,但是如果你不能按照@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中集成单元测试 .

相关问题