【问题标题】:How to write testable requirejs modules如何编写可测试的 requirejs 模块
【发布时间】:2013-11-04 11:59:40
【问题描述】:

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

【问题讨论】:

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


    【解决方案1】:

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

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

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

    我的建议:

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

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

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

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

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

    【讨论】:

    • 您好,感谢您的回答。可能我应该改写我的问题,因为这里真正的问题是我无法模拟内部方法。
    • 但是如果将内部组件放入自己的模块中,那么您可以模拟它们。
    【解决方案2】:

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

    这里有一个例子可以满足你的需要:

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

    我提出了一个可运行的示例here。 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 为假,则该函数不会被导出并且不会被包装。我更新了example 来展示这个方法。示例中为moduleB。

    【讨论】:

    • 嘿,这是可行的,但有点烦人,因为您必须在每个方法调用前加上地图名称。我会等待几天的其他建议:)
    • @GEMI 我已经用另一种方法更新了我的答案。除此之外,唯一想到的是:预处理您的 JavaScript 以添加检测,或使用不可移植的 VM 挂钩来穿透闭包。
    • 如果我早点读到这个。正在绞尽脑汁地思考如何通过闭包来存根带有 sinon 的函数。先生,您为我节省了很多时间!
    猜你喜欢
    • 2014-07-29
    • 1970-01-01
    • 2015-01-18
    • 1970-01-01
    • 1970-01-01
    • 2015-03-16
    • 1970-01-01
    • 2013-08-12
    • 2018-01-26
    相关资源
    最近更新 更多