129

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

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

我怎样才能模拟出来hurpdurp以便有效地进行单元测试?

4

7 回答 7

66

因此,在阅读完这篇文章后,我想出了一个解决方案,它使用 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/'
  });
}

Hurp因此,它创建了一个新的上下文,其中的定义Durp将由您传递给函数的对象设置。名称的 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');
      })
    });
  });
})();

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

于 2012-07-27T20:48:34.880 回答
45

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

来自文档:

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

于 2012-12-11T22:41:04.723 回答
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 中模拟 deps 很糟糕。我找到了一种可行的方法,但我仍然对它不太满意。如果您有更好的想法,请告诉我。

于 2012-07-27T18:48:13.670 回答
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模块,该模块将成为真正的模块引用和相应的存根。

于 2012-08-01T12:14:49.677 回答
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/

于 2012-08-23T17:56:50.250 回答
2

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

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

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

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

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);
                        }
                    });
                });
            });
        });
    });
});
于 2016-08-09T13:08:54.533 回答
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;
于 2015-04-07T14:25:19.293 回答