繁体   English   中英

如何编写可测试的requirejs模块

[英]How to write testable requirejs modules

我是单元测试的新手,所以我可能会遗漏一些东西,但我应该如何构建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
}

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

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

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

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

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

我的建议:

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

在实施和重构阶段,模块的内部将发生变化。 例如,func2可以分成不同的功能。 而且危险在于,如果你专门测试了func2,那么你可能必须在重构时重写测试。

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

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

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

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

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

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选项设置为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为true时,它会导出_dynamic包装的函数, 提供允许引用它们而不经过exports本地符号。 如果debug为false,则该函数不会导出且不会被包装。 我已经更新了示例以显示此方法。 它是moduleB中的moduleB

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM