如何编写可测试的requirejs模块

时间:2013-10-25 11:29:45

标签: javascript unit-testing requirejs jasmine karma-runner

我是单元测试的新手,所以我可能会遗漏一些东西,但我应该如何构建requirejs模块以使它们完全可测试?考虑优雅的揭示模块模式。

define([], function () {
    "use strict";

    var func1 = function(){
        var data = func2();
    };
    var func2 = function(){
        return db.call();
    };

    return {
        func1 : func1
    }
});

据我所知,这是构建requirejs模块的最常见模式。如果我错了请纠正我!因此,在这种简单的场景中,我可以轻松地测试func1的返回值和行为,因为它是全局的。但是,为了测试func2,我还必须返回它的参考。正确?

return {
    func1 : func1,
    _test_func2 : func2
}

这使得代码稍微不那么漂亮,但总体来说仍然可以。但是,如果我想模拟func2并使用Jasmine spy替换其返回值,那么我将无法将该方法置于闭包内。

所以我的问题是如何构建requirejs模块以完全可测试?对于这种情况,是否有比揭示模块模式更好的模式?

2 个答案:

答案 0 :(得分:8)

您确定要测试私有函数func2吗?

我认为开发人员在尝试为私有函数编写测试时忽略了单元测试的重点。

在开发软件时,依赖性是让我们受到欢迎的因素。而且依赖性越强,挤压越紧。因此,如果您有许多依赖于模块内部工作的测试,那么当您想要更改内部实现时,这将非常痛苦。因此,请保持测试依赖于公共接口,并将私有内容保密。

我的建议:

  1. 设计模块的公共接口。
  2. 针对公共接口编写测试以指定某些预期行为。
  3. 实现通过该测试所需的代码。
  4. 重构(如有必要)
  5. 从步骤2开始重复,直到测试定义了所有功能,并且所有测试都通过。
  6. 在实施和重构阶段,模块的内部将发生变化。例如,func2可以分成不同的功能。而且危险在于,如果你专门测试了func2,那么你可能必须在重构时重写测试。

    单元测试的主要好处之一是它们确保在我们更改模块的内部工作时不会破坏现有功能。如果重构意味着您需要更新测试,则会开始失去这种好处。

    如果func2中的代码变得如此复杂以至于您想要显式地测试它,那么将其提取到一个单独的模块中,在该模块中,您可以使用针对公共接口的单元测试来定义行为。瞄准具有易于理解的公共界面的小型,经过良好测试的模块。

    如果您正在寻求有关单元测试的帮助,我会完全推荐Kent Beck的书“TDD by example”。编写糟糕的单元测试将成为一个障碍而不是一个好处,在我看来TDD是唯一的出路。

答案 1 :(得分:6)

如果模块中的函数直接调用模块的其他函数(即通过使用模块本地的引用),则无法从外部拦截这些调用。但是,如果您更改模块以使 in 内的函数以与外部相同的方式调用模块的功能,那么您可以拦截这些调用。

这是一个允许你想要的例子:

define([], function () {
    "use strict";

    var foo = function(){
        return exports.bar();
    };

    var bar = function(){
        return "original";
    };

    var exports =  {
        foo: foo,
        bar: bar
    };

    return exports;
});

关键是foo通过exports访问bar而不是直接调用它。

我已经提出了一个可运行的示例herespec/main.spec.js文件包含:

    expect(moduleA.foo()).toEqual("original");

    spyOn(moduleA, "bar").andReturn("patched");

    expect(moduleA.foo()).toEqual("patched");

您会注意到bar是修补的功能,但foo会受到修补的影响。

另外,为了避免长期受到测试代码污染的导出,我有时会进行环境检查以确定模块是否在测试环境中运行并导出测试所需的功能 < / strong>在测试模式下。这是我写的实际代码的一个例子:

var options = module.config();
var test = options && options.test;

[...]
// For testing only
if (test) {
    exports.__test = {
        $modal: $modal,
        reset: _reset,
        is_terminating: _is_terminating
    };
}

如果requirejs配置配置我的模块(使用config)以使其test选项设置为true值,则导出将另外包含__test符号,其中包含我在测试模块时想要导出的其他几项。否则,这些符号不可用。

编辑:如果您对上述第一种方法的困扰是必须使用exports对内部函数的所有调用加前缀,您可以执行以下操作:

define(["module"], function (module) {
    "use strict";

    var debug = module.config().debug;
    var exports = {};

    /**
     * @function
     * @param {String} name Name of the function to export
     * @param {Function} f Function to export.
     * @returns {Function} A wrapper for <code>f</code>, or <code>f</code>.
     */
    var _dynamic = (debug ?
        function (name, f) {
            exports[name] = f;
            return function () {
                // This call allows for future changes to arguments passed..
                return exports[name].apply(this, arguments);
            };
        } :
        _dynamic = function (name, f) { return f; });

    var foo = function () {
        return bar(1, 2, 3);
    };

    var bar = _dynamic("bar", function (a, b, c) {
        return "original: called with " + a + " " + b + " " + c;
    });

    exports.foo = foo;

    return exports;
});

当RequireJS配置配置上面的模块以使debug为真时,它会导出由_dynamic 包裹的函数,而提供允许引用它们的本地符号而不进行通过exports。如果debug为false,则该函数不会导出,也不会被包装。我已更新example以显示此方法。在示例中为moduleB