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

130

我有一个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函数。不过有几种不同的选择。我会发布一个答案,希望它能有所帮助。 - Jamison Dance
1
对于使用Jasmine进行单元测试,您可能还想快速查看Jasq。[免责声明:我正在维护该库] - biril
1
如果你正在 Node 环境下进行测试,你可以使用 require-mock 包。它允许你轻松地模拟你的依赖项,替换模块等。如果你需要在浏览器环境中进行异步模块加载,你可以尝试 Squire.js - ValeriiVasin
7个回答

66

读完这篇文章后,我想到了一种解决方案,可以使用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文件将作为普通依赖项加载。 - Andreas Köberle
1
我在Rails中使用它(与jasminerice/phantomjs一起),这是我发现的最好的用于使用RequireJS进行模拟的解决方案。 - Ben Anderson
13
虽然不是很美观,但在所有可能的解决方案中,这似乎是最不丑陋/混乱的一个。这个问题值得更多的关注。 - Chris Salzberg
1
更新:对于任何考虑此解决方案的人,我建议查看下面提到的squire.js(https://github.com/iammerrick/Squire.js/)。它是一个很好的实现类似于这个解决方案的解决方案,可以在需要存根的地方创建新的上下文。 - Chris Salzberg
显示剩余6条评论

46
你可能希望查看新的Squire.js库
从文档中可以看到: Squire.js是Require.js用户的依赖注入器,使模拟依赖项变得容易!

2
强烈推荐!我正在更新我的代码,使用squire.js,到目前为止我非常喜欢它。非常简单的代码,没有什么神奇的东西在内部,但是以一种相对容易理解的方式完成。 - Chris Salzberg
2
我曾经遇到过很多问题,其中之一就是squire会影响其他测试,因此我无法推荐它。我建议使用https://www.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文件来为每个依赖项定义路径,指向模拟而不是原始依赖项。这也很丑陋,需要创建大量的测试文件和配置文件。

在Node中伪造它

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

您可以创建自己的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. 在node中运行测试,因此无需与浏览器自动化纠缠。
  2. 测试中不需要太多AMD样板文件。
  3. 你可以愉快地使用eval,然后想象Crockford的怒火爆发。

显然还有一些缺点。

  1. 由于您正在在node中进行测试,因此无法处理浏览器事件或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'}} - AlignedDev

9

2
我真的很想让testr.js起作用,但它似乎还不够胜任。最终,我选择了@Andreas Köberle的解决方案,它将在我的测试中添加嵌套上下文(不太美观),但它始终可行。我希望有人能够专注于以更优雅的方式解决这个问题。我会继续关注testr.js,如果/当它起作用时,我会进行切换。 - Chris Salzberg
@shioyama 你好,感谢您的反馈!我很想看看您如何在测试堆栈中配置testr.js。如果您遇到任何问题,我很乐意帮助您解决!如果您想记录一些内容,还可以使用github Issues页面。谢谢。 - Matty F
1
@MattyF 很抱歉,我现在甚至无法回忆起 testr.js 为什么不能工作的确切原因,但我得出了这样的结论:使用额外的上下文实际上是非常好的,事实上符合 require.js 用于模拟/存根的使用方式。 - Chris Salzberg

2
这个答案是基于Andreas Köberle的回答的。
对于我来说,实现和理解他的解决方案并不容易,因此我将详细解释它的工作原理以及一些需要避免的陷阱,希望能帮助未来的访问者。

首先是设置:
我使用Karma作为测试运行器,MochaJs作为测试框架。

使用类似Squire的东西对我来说没有用,由于某种原因,当我使用它时,测试框架会抛出错误:

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

RequireJs有可能将模块ID映射到其他模块ID上。它还允许创建一个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;

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