如何在 RequireJS 中模拟单元测试的依赖项?

Posted

技术标签:

【中文标题】如何在 RequireJS 中模拟单元测试的依赖项?【英文标题】:How can I mock dependencies for unit testing in RequireJS? 【发布时间】:2012-07-11 11:27:42 【问题描述】:

我有一个要测试的 AMD 模块,但我想模拟它的依赖关系而不是加载实际的依赖关系。我正在使用 requirejs,我的模块代码如下所示:

define(['hurp', 'durp'], function(Hurp, Durp) 
  return 
    foo: function () 
      console.log(Hurp.beans)
    ,
    bar: function () 
      console.log(Durp.beans)
    
  

如何模拟 hurpdurp 以便进行有效的单元测试?

【问题讨论】:

我只是在 node.js 中做一些疯狂的 eval 东西来模拟 define 函数。不过有几个不同的选择。我会发布一个答案,希望它会有所帮助。 对于 Jasmine 的单元测试,您可能还想快速查看 Jasq。 [免责声明:我正在维护库] 如果您在节点环境中进行测试,您可以使用require-mock 包。它允许您轻松地模拟您的依赖项、替换模块等。如果您需要带有异步模块加载的浏览器环境 - 您可以尝试Squire.js 【参考方案1】:

所以在阅读this post 之后,我想出了一个解决方案,它使用 requirejs 配置函数为您的测试创建一个新上下文,您可以在其中简单地模拟您的依赖项:

var cnt = 0;
function createContext(stubs) 
  cnt++;
  var map = ;

  var i18n = stubs.i18n;
  stubs.i18n = 
    load: sinon.spy(function(name, req, onLoad) 
      onLoad(i18n);
    )
  ;

  _.each(stubs, function(value, key) 
    var stubName = 'stub' + key + cnt;

    map[key] = stubName;

    define(stubName, function() 
      return value;
    );
  );

  return require.config(
    context: "context_" + cnt,
    map: 
      "*": map
    ,
    baseUrl: 'js/cfe/app/'
  );

因此它创建了一个新的上下文,其中HurpDurp 的定义将由您传递给函数的对象设置。名称的 Math.random 可能有点脏,但它有效。因为如果您要进行大量测试,您需要为每个套件创建新的上下文以防止重复使用您的模拟,或者在您需要真正的 requirejs 模块时加载模拟。

在你的情况下,它看起来像这样:

(function () 

  var stubs =  
    hurp: 'hurp',
    durp: 'durp'
  ;
  var context = createContext(stubs);

  context(['yourModuleName'], function (yourModule) 

    //your normal jasmine test starts here

    describe("yourModuleName", function () 
      it('should log', function()
         spyOn(console, 'log');
         yourModule.foo();

         expect(console.log).toHasBeenCalledWith('hurp');
      )
    );
  );
)();

所以我在生产中使用这种方法有一段时间了,它确实很强大。

【讨论】:

我喜欢你在这里所做的……特别是因为你可以为每个测试加载不同的上下文。我唯一希望我能改变的是,它似乎只有在我模拟出所有依赖项时才有效。如果没有提供模拟对象,您是否知道返回模拟对象的方法,但如果没有提供模拟对象,则回退到从实际的 .js 文件中检索?我一直在尝试挖掘需要的代码来弄清楚,但我有点迷路了。 它只模拟你传递给createContext函数的依赖。因此,在您的情况下,如果您仅将 hurp: 'hurp' 传递给函数,则 durp 文件将作为正常依赖项加载。 我在 Rails 中使用它(使用 jasminerice/phantomjs),它是我找到的使用 RequireJS 模拟的最佳解决方案。 +1 不漂亮,但在所有可能的解决方案中,这似乎是最不丑/最不乱的一个。这个问题值得更多关注。 更新:对于考虑使用此解决方案的任何人,我建议查看下面提到的 squire.js (github.com/iammerrick/Squire.js)。这是一个很好的解决方案实现,类似于这个解决方案,在需要存根的地方创建新的上下文。【参考方案2】:

您可能想查看新的Squire.js lib

来自文档:

Squire.js 是一个依赖注入器,供 Require.js 用户轻松模拟依赖!

【讨论】:

强烈推荐!我正在更新我的代码以使用 squire.js,到目前为止我非常喜欢它。非常非常简单的代码,引擎盖下没有什么神奇之处,但以一种(相对)容易理解的方式完成。 我在 squire 影响其他测试方面遇到了很多问题,因此无法推荐。我会推荐npmjs.com/package/requirejs-mock【参考方案3】:

我为这个问题找到了三种不同的解决方案,但没有一个是令人愉快的。

内联定义依赖

define('hurp', [], function () 
  return 
    beans: 'Beans'
  ;
);

define('durp', [], function () 
  return 
    beans: 'durp beans'
  ;
);

require('hurpdhurp', function () 
  // test hurpdurp in here
);

丑陋。你必须用大量的 AMD 样板来弄乱你的测试。

从不同路径加载 Mock 依赖项

这涉及使用单独的 config.js 文件为每个指向模拟而不是原始依赖项的依赖项定义路径。这也很丑陋,需要创建大量的测试文件和配置文件。

在节点中伪造它

这是我目前的解决方案,但仍然很糟糕。

您创建自己的define 函数来为模块提供您自己的模拟并将您的测试放在回调中。然后你 eval 模块来运行你的测试,像这样:

var fs = require('fs')
  , hurp = 
      beans: 'BEANS'
    
  , durp = 
      beans: 'durp beans'
    
  , hurpDurp = fs.readFileSync('path/to/hurpDurp', 'utf8');
  ;



function define(deps, cb) 
  var TestableHurpDurp = cb(hurp, durp);
  // now run tests below on TestableHurpDurp, which is using your
  // passed-in mocks as dependencies.


// evaluate the AMD module, running your mocked define function and your tests.
eval(hurpDurp);

这是我的首选解决方案。它看起来有点神奇,但它有一些好处。

    在节点中运行您的测试,因此不会干扰浏览器自动化。 在您的测试中减少对凌乱的 AMD 样板的需求。 您可以在愤怒时使用 eval,并想象 Crockford 会怒火中烧。

显然,它仍然有一些缺点。

    由于您在节点中进行测试,因此您无法对浏览器事件或 DOM 操作进行任何操作。仅适用于测试逻辑。 设置起来还是有点笨拙。您需要在每个测试中模拟 define,因为这是您的测试实际运行的地方。

我正在开发一个测试运行器,以便为这类东西提供更好的语法,但我仍然没有解决问题 1 的好方法。

结论

在 requirejs 中模拟 deps 很糟糕。我找到了一种可行的方法,但我仍然对它不太满意。如果您有更好的想法,请告诉我。

【讨论】:

【参考方案4】:

有一个config.map 选项http://requirejs.org/docs/api.html#config-map。

关于如何使用它:

    定义普通模块; 定义存根模块;

    显式配置RequireJS;

    requirejs.config(
      map: 
        'source/js': 
          'foo': 'normalModule'
        ,
        'source/test': 
          'foo': 'stubModule'
        
      
    );
    

在这种情况下,对于普通代码和测试代码,您可以使用 foo 模块,这将是真正的模块引用和相应的存根。

【讨论】:

这种方法对我来说非常有效。就我而言,我将此添加到测试运行器页面的 html -> map: '*': 'Common/Modules/usefulModule': '/Tests/Specs/Common/usefulModuleMock.js' 【参考方案5】:

您可以使用testr.js 模拟依赖项。您可以将 testr 设置为加载模拟依赖项而不是原始依赖项。这是一个示例用法:

var fakeDep = function()
    this.getText = function()
        return 'Fake Dependancy';
    ;
;

var Module1 = testr('module1', 
    'dependancies/dependancy1':fakeDep
);

也请查看:http://cyberasylum.janithw.com/mocking-requirejs-dependencies-for-unit-testing/

【讨论】:

我真的很想让 testr.js 工作,但感觉它还不能胜任这项任务。最后,我将使用@Andreas Köberle 的解决方案,该解决方案将为我的测试添加嵌套上下文(不漂亮),但始终有效。我希望有人可以专注于以更优雅的方式解决此解决方案。我会继续关注 testr.js,如果/当它工作时,将进行切换。 @shioyama 嗨,感谢您的反馈!我很想看看您是如何在测试堆栈中配置 testr.js 的。很高兴帮助您解决您可能遇到的任何问题!如果你想在那里记录一些东西,还有 github 问题页面。谢谢, @MattyF 抱歉,我现在什至不记得 testr.js 对我不起作用的确切原因,但我得出的结论是使用额外的上下文是实际上很好,实际上符合 require.js 用于模拟/存根的方式。【参考方案6】:

此答案基于Andreas Köberle's answer。 实施和理解他的解决方案对我来说并不容易,所以我将更详细地解释它是如何工作的,以及需要避免的一些陷阱,希望它对未来的访问者有所帮助。

所以,首先设置: 我使用 Karma 作为测试运行程序,使用 MochaJs 作为测试框架。

使用 Squire 之类的东西对我不起作用,由于某种原因,当我使用它时,测试框架抛出了错误:

TypeError:无法读取未定义的属性“调用”

RequireJs 可以将 map 模块 ID 转换为其他模块 ID。它还允许创建一个require function,它使用different config,而不是全局require。 这些功能对于此解决方案的运行至关重要。

这是我的模拟代码版本,包括(很多)cmets(我希望它可以理解)。我将它包装在一个模块中,以便测试可以轻松地使用它。

define([], function () 
    var count = 0;
    var requireJsMock= Object.create(null);
    requireJsMock.createMockRequire = function (mocks) 
        //mocks is an object with the module ids/paths as keys, and the module as value
        count++;
        var map = ;

        //register the mocks with unique names, and create a mapping from the mocked module id to the mock module id
        //this will cause RequireJs to load the mock module instead of the real one
        for (property in mocks) 
            if (mocks.hasOwnProperty(property)) 
                var moduleId = property;  //the object property is the module id
                var module = mocks[property];   //the value is the mock
                var stubId = 'stub' + moduleId + count;   //create a unique name to register the module

                map[moduleId] = stubId;   //add to the mapping

                //register the mock with the unique id, so that RequireJs can actually call it
                define(stubId, function () 
                    return module;
                );
            
        

        var defaultContext = requirejs.s.contexts._.config;
        var requireMockContext =  baseUrl: defaultContext.baseUrl ;   //use the baseUrl of the global RequireJs config, so that it doesn't have to be repeated here
        requireMockContext.context = "context_" + count;    //use a unique context name, so that the configs dont overlap
        //use the mapping for all modules
        requireMockContext.map = 
            "*": map
        ;
        return require.config(requireMockContext);  //create a require function that uses the new config
    ;

    return requireJsMock;
);

我遇到的最大的陷阱是创建 RequireJs 配置,这确实花了我几个小时。我试图(深度)复制它,并且只覆盖必要的属性(如上下文或地图)。这不起作用!只复制baseUrl,这样就可以了。

用法

要使用它,请在测试中使用它,创建模拟,然后将其传递给 createMockRequire。例如:

var ModuleMock = function () 
    this.method = function () 
        methodCalled += 1;
    ;
;
var mocks = 
    "ModuleIdOrPath": ModuleMock

var requireMocks = mocker.createMockRequire(mocks);

这里是一个完整测试文件的示例

define(["chai", "requireJsMock"], function (chai, requireJsMock) 
    var expect = chai.expect;

    describe("Module", function () 
        describe("Method", function () 
            it("should work", function () 
                return new Promise(function (resolve, reject) 
                    var handler =  handle: function ()   ;

                    var called = 0;
                    var moduleBMock = function () 
                        this.method = function () 
                            methodCalled += 1;
                        ;
                    ;
                    var mocks = 
                        "ModuleBIdOrPath": moduleBMock
                    
                    var requireMocks = requireJsMock.createMockRequire(mocks);

                    requireMocks(["js/ModuleA"], function (moduleA) 
                        try 
                            moduleA.method();   //moduleA should call method of moduleBMock
                            expect(called).to.equal(1);
                            resolve();
                         catch (e) 
                            reject(e);
                        
                    );
                );
            );
        );
    );
);

【讨论】:

【参考方案7】:

如果你想做一些简单的js测试来隔离一个单元,那么你可以简单地使用这个sn-p:

function define(args, func)
    if(!args.length)
        throw new Error("please stick to the require.js api which wants a: define(['mydependency'], function())");
    

    var fileName = document.scripts[document.scripts.length-1].src;

    // get rid of the url and path elements
    fileName = fileName.split("/");
    fileName = fileName[fileName.length-1];

    // get rid of the file ending
    fileName = fileName.split(".");
    fileName = fileName[0];

    window[fileName] = func;
    return func;

window.define = define;

【讨论】:

以上是关于如何在 RequireJS 中模拟单元测试的依赖项?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Service 构造函数中对 Controller 进行单元测试和模拟 @InjectModel

如何使用 ES6 模块模拟单元测试的依赖关系

如何在 Grails 单元测试中使用 Spock 模拟 passwordEncoder

测试具有 RequireJS 依赖项的 es6 模块时,Jest 中的“未定义定义”

在 Java Play 2.4 中测试具有模拟依赖项的控制器

单元测试-桩对象