如何对模块的“根”函数进行require() / expect调用的存根处理?

8
考虑以下茉莉规范:
describe("something.act()", function() {
  it("calls some function of my module", function() {
    var mod = require('my_module');
    spyOn(mod, "someFunction");
    something.act();
    expect(mod.someFunction).toHaveBeenCalled();
  });
});

这个工作很完美。类似这样的东西会让它变成绿色:
something.act = function() { require('my_module').someFunction(); };

现在看一下这个:
describe("something.act()", function() {
  it("calls the 'root' function of my module", function() {
    var mod = require('my_module');
    spyOn(mod); // jasmine needs a property name
                // pointing to a function as param #2
                // therefore, this call is not correct.
    something.act();
    expect(mod).toHaveBeenCalled(); // mod should be a spy
  });
});

这是我想用这个规范测试的代码:

something.act = function() { require('my_module')(); };

在过去的几个月中,这个问题已经困扰了我好几次。一个理论上的解决方案是用createSpy()替换require()并返回一个spy。但是,require()是一个不可阻挡的野兽:它是每个源文件/模块中不同的函数"副本"。在测试规范中对其进行打桩(stubbing)将无法替换"testee"源文件中真正的require()函数。

另一种选择是向加载路径添加一些假模块,但对我来说看起来太复杂了。

有什么想法吗?

6个回答

6

rewire 对于这个非常棒。

var rewire = require('rewire');

describe("something.act()", function() {
  it("calls the 'root' function of my module", function() {
    var mod = rewire('my_module');
    var mockRootFunction = jasmine.createSpy('mockRootFunction');
    var requireSpy = {
      mockRequire: function() {
        return mockRootFunction;
      }
    };
    spyOn(requireSpy, 'mockRequire').andCallThrough();

    origRequire = mod.__get__('require');
    mod.__set__('require', requireSpy.mockRequire);

    something.act();
    expect(requireSpy.mockRequire).toHaveBeenCalledWith('my_module');
    expect(mockRootFunction).toHaveBeenCalled();

    mod.__set__('require', origRequire);
  });
});

1
哇,谢谢你。我之前不知道 rewire 这个东西。这真是一个很好的解决方案来解决这个问题。 - Terrence

5

看起来我找到了一个可接受的解决方案。

规范助手:

var moduleSpies = {};
var originalJsLoader = require.extensions['.js'];

spyOnModule = function spyOnModule(module) {
  var path          = require.resolve(module);
  var spy           = createSpy("spy on module \"" + module + "\"");
  moduleSpies[path] = spy;
  delete require.cache[path];
  return spy;
};

require.extensions['.js'] = function (obj, path) {
  if (moduleSpies[path])
    obj.exports = moduleSpies[path];
  else
    return originalJsLoader(obj, path);
}

afterEach(function() {
  for (var path in moduleSpies) {
    delete moduleSpies[path];
  }
});

规范:
describe("something.act()", function() {
  it("calls the 'root' function of my module", function() {
    var mod = spyOnModule('my_module');
    something.act();
    expect(mod).toHaveBeenCalled(); // mod is a spy
  });
});

这并不完美,但是它能够很好地完成工作。它甚至不会干扰被测试的源代码,这对我来说是一种标准。


这对于一个规范完美地运作,但当我尝试运行两个规范,它们都单独存根相同的模块时,第一个存根总是返回给两个规范。它被缓存在某个地方,但我无法找出在哪里。delete moduleSpies[path] 不够好,似乎不起作用。 - EndangeredMassa
delete moduleSpies[path];之后尝试使用delete require.cache[path];,你可以试试吗? - jbpros

2
我今天需要做这件事情,然后看到了这篇文章。我的解决方案如下:
在一个规范的帮助程序中:
var originalRequire = require;
var requireOverrides = {};

stubModule = function(name) {
  var double = originalRequire(name);
  double['double'] = name;
  requireOverrides[name] = double;
  return double;
}

require = function(name) {
  if (requireOverrides[name]) {
    return requireOverrides[name];
  } else {
    return originalRequire(name);
  }
}

afterEach(function() {
  requireOverrides = {};
});

在一个规范中:

AWS = stubModule('aws-sdk');
spyOn(AWS.S3, 'Client');

// do something

expect(AWS.S3.Client).toHaveBeenCalled();

1

这非常有帮助,但它不支持通过.andCallThrough()进行调用。

我能够进行适应,所以我想分享一下:

function clone(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  var key;
  var temp = new obj.constructor();
  for (key in obj) {
    if (obj.hasOwnProperty(key)) {
      temp[key] = clone(obj[key]);
    }
  }
  return temp;
};

spyOnModule = function spyOnModule(name) {
  var path          = require.resolve(name);
  var spy           = createSpy("spy on module \"" + name + "\"");
  moduleSpies[path] = spy;

  // Fake calling through
  spy.andCallThrough = function() {

    // Create a module object
    var mod = clone(module);
    mod.parent = module;
    mod.id = path;
    mod.filename = path;

    // Load it backdoor
    originalJsLoader(mod, path);

    // And set it's export as a faked call
    return this.andCallFake(mod.exports);
  }

  delete require.cache[path];
  return spy;
};

0

你可以使用 gently 模块 (https://github.com/felixge/node-gently)。在示例中提到了劫持 require,而 dirty NPM 模块积极地使用它,所以我认为它是有效的。


谢谢你的建议!我看了一下那些示例,但并不是很喜欢。实际上,这只是我最初采取的相同方法,它会直接替换源文件中的 require() 函数:if (global.GENTLY) require = GENTLY.hijack(require); - jbpros

-1

还有另一种方法。当需要时,您可以通过不使用var将模块放在全局范围内:

someModule = require('someModule');

describe('whatever', function() {
  it('does something', function() {
    spyOn(global, 'someModule');

    someFunctionThatShouldCallTheModule();

    expect(someModule).toHaveBeenCalled();
  }
}

你还可以将模块包装在另一个模块中:
//someModuleWrapper.js
require('someModule');

function callModule(arg) {
  someModule(arg);
}
exports.callModule = callModule;

//In the spec file:
someModuleWrapper = require('someModuleWrapper');

describe('whatever', function() {
  it('does something', function() {
    spyOn(someModuleWrapper, 'callModule');

    someFunctionThatShouldCallTheModule();

    expect(someModuleWrapper.callModule).toHaveBeenCalled();
  }
}

然后显然要确保无论在哪里调用someFunctionThatShouldCallTheModule,都需要引用包装器而不是真正的模块。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接