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


187

我试图弄清楚如何在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
也许只是在特定环境中公开要测试的功能?我在这里不知道标准程序。
loganfsmyth 2013年

2
没有导出它是有原因的。仅测试公共接口,所有私有接口都将进行测试。
亚历杭德罗

Answers:


255

联控模块是绝对的答案。

这是我的代码,用于访问未导出的函数并使用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();
  });
});

2
这绝对是最佳答案。它不需要重写所有具有NODE_ENV特定导出的现有模块,也不需要以文本形式读取模块。
亚当·约斯特

美丽的解决方案。可以进一步将其与间谍程序集成到您的测试框架中。我与茉莉花一起尝试了这种策略
佛朗哥

2
很好的解决方案。有针对Babel型人士的工作版本吗?
Charles Merriam

2
在jest和ts-jest(打字稿)中使用rewire时,出现以下错误:Cannot find module '../../package' from 'node.js'。你见过这个吗?
b.lit

3
Rewire有玩笑的兼容性问题。Jest不会在覆盖率报告中考虑从rewire调用的功能。这有点违背了目的。
robross0606

12

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


9
这似乎是一种hack,如果没有执行NODE_ENV块,是否真的没有办法测试内部(非导出)功能?
RyanHirsch 2014年

3
真讨厌 这不是解决此问题的最佳方法。
npiv 2014年

7

编辑:

使用加载模块vm可能会导致意外行为(例如,instanceof操作员不再使用在此类模块中创建的对象,因为全局原型与通常使用加载的模块中使用的原型不同require)。我不再使用以下技术,而是使用重新接线模块。效果很好。这是我的原始答案:

详细说明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;};

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

2
您能否举个例子,说明如何使用访问未导出的函数rewire
Matthias 2015年

1
嗨,马提亚斯,我已经在您的回答中举了一个例子。如果您喜欢,也许赞成我的几个问题?:)几乎我所有的问题都位于0,StackOverflow正在考虑冻结我的问题。X_X
安东尼

2

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

0

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


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并且在生产中不会将其导出


0

不建议您这样做,但是如果您不能rewire按照@Antoine的建议使用,则始终可以读取文件并使用eval()

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

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

JS文件将在window没有任何require(...)andmodule.exports语句的情况下设置许多全局变量(始终没有像Webpack或Browserify这样的模块打包器可用于删除这些语句)。

与其重构整个代码库,不如让我们将单元测试集成到我们的客户端JS中。


确定的创意解决方案
notacorn
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.