【问题标题】:How to stub require() / expect calls to the "root" function of a module?如何存根 require() /期望调用模块的“根”函数?
【发布时间】:2011-10-23 06:30:49
【问题描述】:

考虑以下茉莉花规格:

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

在过去的几个月里,这让我多次陷入困境。一种理论上的解决方案是替换 require() 并返回使用 createSpy() 创建的间谍。但是 require() 是不可阻挡的野兽:它是每个源文件/模块中函数的不同“副本”。在规范中将其存根不会替换“testee”源文件中真正的 require() 函数。

另一种方法是在加载路径中添加一些假模块,但对我来说它看起来太复杂了。

有什么想法吗?

【问题讨论】:

    标签: javascript node.js bdd stub jasmine


    【解决方案1】:

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

    【讨论】:

    • 哇,谢谢。我不知道重新布线。这个问题的解决方案真是太好了。
    【解决方案2】:

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

    规范助手:

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

    这并不完美,但做得很好。它甚至不会与 testee 源代码混淆,这对我来说是一种标准。

    【讨论】:

    • 这对一个规范非常有效,但是当我尝试运行两个分别存根同一模块的规范时,总是为这两个规范返回第一个存根。它被缓存在某个地方,但我不知道在哪里。 delete moduleSpies[path] 似乎不够好。
    • 你能在delete moduleSpies[path];之后尝试delete require.cache[path];吗?
    【解决方案3】:

    我今天需要这样做,并遇到了这篇文章。我的解决方案如下:

    在规范助手中:

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

    【讨论】:

      【解决方案4】:

      这很有帮助,但不支持通过.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;
      };
      

      【讨论】:

        【解决方案5】:

        您可以使用轻柔的模块(https://github.com/felixge/node-gently)。示例中提到了劫持require,脏NPM模块积极使用它,所以我想它可以工作。

        【讨论】:

        • 感谢您的提示!我浏览了这些示例,但并没有找到它。实际上,这与我最初采用的方法相同;它替换了源文件中的 require() :if (global.GENTLY) require = GENTLY.hijack(require);.
        【解决方案6】:

        还有另一种方法。您可以在需要时不使用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 在哪里,您都需要包装器而不是真正的模块。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-05-11
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多