8

我是单元测试的新手,所以我可能会遗漏一些东西,但是我应该如何构建 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 模块以使其完全可测试?对于这种情况,是否有比显示模块模式更好的模式?

4

2 回答 2

8

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

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

在开发软件时,依赖关系让我们大吃一惊。而且依赖越多,挤压越紧。因此,如果您有很多依赖于模块内部工作的测试,那么当您想要更改内部实现时将会非常痛苦。所以让你的测试依赖于公共接口,并保持私有的东西私有。

我的建议:

  1. 设计模块的公共接口。
  2. 针对公共接口编写测试以指定一些预期的行为。
  3. 实现通过该测试所需的代码。
  4. 重构(如有必要)
  5. 从步骤 2 开始重复,直到测试定义了所有功能,并且所有测试都通过了。

在实现和重构阶段,模块的内部结构会发生变化。例如,func2 可以拆分为不同的函数。危险在于,如果你有专门针对 func2 的测试,那么在重构时你可能不得不重写测试。

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

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

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

于 2013-10-30T14:51:57.823 回答
6

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

这是一个允许您想要的示例:

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而不是直接调用它。

我在这里提出了一个可运行的示例。该spec/main.spec.js文件包含:

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

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

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

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

此外,为了避免导出被测试代码永久污染,我有时会进行环境检查以确定模块是否在测试环境中运行,并在测试模式下导出测试所需的功能。这是我编写的实际代码示例:

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选项设置为真值,则导出将另外包含一个__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为 true 时,它​​会导出被包装的函数_dynamic 提供本地符号,允许在不经过exports. 如果debug为 false,则该函数不会被导出并且不会被包装。我已更新示例以显示此方法。它moduleB在示例中。

于 2013-10-28T11:31:24.873 回答