如何在RequireJS中模拟依赖关系以进行单元测试?


127

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

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

我该如何模拟hurpdurp有效地进行单元测试?


我只是在node.js中做一些疯狂的评估工作,以模拟该define功能。虽然有一些不同的选择。我将发布答案,希望对您有所帮助。
jergason 2012年

1
对于使用Jasmine进行单元测试,您可能还希望快速了解Jasq。[免责声明:我维护的是lib]
biril 2014年

1
如果要在节点环境中进行测试,则可以使用require-mock程序包。它使您可以轻松地模拟依赖项,替换模块等。如果您需要异步模块加载的浏览器环境-您可以尝试Squire.js
ValeriiVasin 2015年

Answers:


64

因此,在阅读了这篇文章之后,我想出了一个使用requirejs config函数为您的测试创建新上下文的解决方案,您可以在其中轻松模拟依赖项:

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

因此,我在生产中使用这种方法已有一段时间,而且它确实很可靠。


1
我喜欢您在这里所做的事情……尤其是因为您可以为每个测试加载不同的上下文。我唯一希望我可以更改的是,它似乎只有在我模拟掉所有依赖项时才有效。您是否知道一种方法来返回模拟对象(如果存在),但是如果没有提供模拟,则回退到从实际的.js文件中检索?我一直在尝试研究require代码以弄清楚它,但是我有点迷路了。
Glen Hughes

5
它仅模拟您传递给createContext函数的依赖项。因此,在您的情况下,如果仅传递{hurp: 'hurp'}给函数,则durp文件将作为常规依赖项加载。
AndreasKöberle'12

1
我在Rails(使用jasminerice / phantomjs)中使用了它,这是我发现的使用RequireJS进行模拟的最佳解决方案。
本·安德森

13
+1不太漂亮,但是在所有可能的解决方案中,这似乎是最少的丑陋/混乱的解决方案。这个问题值得更多关注。
克里斯·萨尔茨伯格

1
更新:对于考虑此解决方案的任何人,我建议您查看下面提到的squire.js(github.com/iammerrick/Squire.js)。这是一种类似于此解决方案的不错的实现,可以在需要存根的地方创建新的上下文。
克里斯·萨尔茨贝格

44

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

从文档:

Squire.js是Require.js用户的依赖注入器,可以简化模拟依赖!


2
强力推荐!我正在更新代码以使用squire.js,到目前为止,我非常喜欢它。非常非常简单的代码,没有什么强大的魔术,但是以(相对)易于理解的方式完成。
克里斯·

1
我在乡绅副作用其他测试方面有很多问题,不能推荐。我会推荐npmjs.com/package/requirejs-mock
Jeff Whiting

17

我已经找到了解决此问题的三种不同解决方案,但都不令人满意。

内联定义依赖项

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

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

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

不好意思 您必须使用大量AMD样板来弄乱测试。

从不同的路径加载模拟依赖

这涉及到使用单独的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);

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

  1. 在节点中运行测试,因此不会干扰浏览器自动化。
  2. 测试中对凌乱的AMD样板的需求更少。
  3. 您会eval在愤怒中使用,并想象Crockford愤怒地爆炸。

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

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

我正在测试赛跑者上为这种东西提供更好的语法,但是对于问题1我仍然没有好的解决方案。

结论

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


15

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

关于使用方法:

  1. 定义普通模块;
  2. 定义存根模块;
  3. 明确配置RequireJS;

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

在这种情况下,对于常规代码和测试代码,您可以使用foo模块,该模块将成为实际的模块引用并相应地存根。


这种方法对我来说真的很好。就我而言,我将其添加到测试运行器页面的html-> map:{'*':{'Common / Modules / usefulModule':'/Tests/Specs/Common/usefulModuleMock.js'}
对齐

9

您可以使用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/


2
我确实希望testr.js能够正常工作,但是还不能完全胜任该任务。最后,我将使用@AndreasKöberle的解决方案,该解决方案将嵌套上下文添加到我的测试中(不是很漂亮),但始终有效。我希望有人可以专注于以更优雅的方式解决此解决方案。我将继续观察testr.js,如果/当它起作用时,将进行切换。
克里斯·萨尔茨伯格

@shioyama嗨,感谢您的反馈!我很想看看您是如何在测试堆栈中配置testr.js的。很高兴帮助您解决可能遇到的任何问题!如果您想在此登录某些内容,则还有github Issues页面。谢谢,
Matty F

1
@MattyF对不起,我现在甚至都不记得testr.js对我不起作用的确切原因是什么,但是我得出的结论是,使用额外的上下文实际上是可以的,并且实际上符合要求以及如何将require.js用于模拟/存根。
克里斯·萨尔茨贝格

2

该答案基于AndreasKöberle的答案
对我来说,实施和理解他的解决方案并不容易,所以我将更详细地解释它的工作原理,并避免一些陷阱,希望它能对将来的访问者有所帮助。

因此,首先进行设置:
我使用Karma作为测试运行程序,并使用MochaJs作为测试框架。

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

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

RequireJs可以模块ID 映射到其他模块ID。它还允许创建使用与global 不同的配置require函数。 这些功能对于该解决方案的工作至关重要。require

这是我的模拟代码版本,包括(很多)注释(我希望它是可以理解的)。我将其包装在模块中,以便测试可以轻松地需要它。

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

0

如果您想进行一些隔离一个单元的普通js测试,则只需使用以下代码段即可:

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;
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.