GEM*_*EMI 8 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
}
});
Run Code Online (Sandbox Code Playgroud)
据我所知,这是构建requirejs模块的最常见模式.如果我错了请纠正我!因此,在这种简单的场景中,我可以轻松地测试返回值和行为,func1因为它是全局的.但是,为了测试func2我也必须返回它的参考.对?
return {
func1 : func1,
_test_func2 : func2
}
Run Code Online (Sandbox Code Playgroud)
这使得代码稍微不那么漂亮,但整体仍然可以.但是,如果我想func2通过使用模拟和替换它的返回值, Jasmine spy我将无法,因为该方法在闭包内.
所以我的问题是如何构建requirejs模块以完全可测试?对于这种情况,是否有比揭示模块模式更好的模式?
你确定要测试私有函数func2吗?
我认为开发人员在尝试为私有函数编写测试时忽略了单元测试的重点.
在开发软件时,依赖性是让我们受到欢迎的因素.而且依赖性越强,挤压越紧.因此,如果您有许多依赖于模块内部工作的测试,那么当您想要更改内部实现时,这将非常痛苦.因此,请保持测试依赖于公共接口,并将私有内容保密.
我的建议:
在实施和重构阶段,模块的内部将发生变化.例如,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;
});
Run Code Online (Sandbox Code Playgroud)
关键是foo通过exports访问bar而不是直接调用它.
我在这里提出了一个可运行的例子.该spec/main.spec.js文件包含:
expect(moduleA.foo()).toEqual("original");
spyOn(moduleA, "bar").andReturn("patched");
expect(moduleA.foo()).toEqual("patched");
Run Code Online (Sandbox Code Playgroud)
您会注意到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
};
}
Run Code Online (Sandbox Code Playgroud)
如果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;
});
Run Code Online (Sandbox Code Playgroud)
当RequireJS配置将上面的模块配置debug为true时,它会导出包装的函数,_dynamic 并提供允许引用它们的本地符号而无需经过exports.如果debug为false,则该函数不会导出且不会被包装.我已经更新了示例以显示此方法.就moduleB在这个例子中.