现在,测试套件看起来如下:
module("dataStore"); test("pop", function() { var dataStore = newDataStore(); dataStore.push("foo"); dataStore.push("bar") equal(dataStore.pop(), "bar", "popping returns the most-recently pushed item"); }); test("length", function() { var dataStore = newDataStore(); dataStore.push("foo"); equal(dataStore.length(), 1, "adding 1 item makes the length 1"); }); |
这让我们的全局 dataStore 和以前的行为保持一致,同时避免了测试之间的相互污染。每项测试都有自己的DataStore 实例对象,都会在测试完成时进入垃圾回收。
避免基于闭包的私有形式
我过去所推崇的另一个模式是 在 JavaScript 中建立真正的私有成员。这样做的好处是,可以保持全局可访问的命名空间免受不必要的,私有实现引用细节的侵扰。然而过度使用这种模式会导致代码无法测试。这是因为你的测试套件将无法访问到闭包中隐藏的私有函数,也就无法进行测试了。考虑以下的代码:
function Templater() { function supplant(str, params) { for (var prop in params) { str.split("{" + prop +"}").join(params[prop]); } return str; } var templates = {}; this.defineTemplate = function(name, template) { templates[name] = template; }; this.render = function(name, params) { if (typeof templates[name] !== "string") { throw "Template " + name + " not found!"; } return supplant(templates[name], params); }; } |
Templater 对象中的关键方法是 supplant,但是我们并不能从构造器闭包的外部访问到此方法。所以,与 QUnit 类似的测试套件并不能如我们期待的那般工作。另外,我们无法在不尝试调用 .render() 方法,让它作用于模板,查看所生成异常的情况下来验证 defineTemplate 方法的效果。我们当然可以简单地添加一个 getTemplate() 方法,并为了测试而把方法暴露为公有接口,但这并不是一件好的做法。在这个简单示例中这么做可能问题不大,但是在构建复杂对象的时候,如果使用了重要的私有方法,将会导致依赖不可测试的标红代码。这里是上面代码的可测试版本:
function Templater() { this._templates = {}; } Templater.prototype = { _supplant: function(str, params) { for (var prop in params) { str.split("{" + prop +"}").join(params[prop]); } return str; }, render: function(name, params) { if (typeof this._templates[name] !== "string") { throw "Template " + name + " not found!"; } return this._supplant(this._templates[name], params); }, defineTemplate: function(name, template) { this._templates[name] = template; } }; |
这里是对应的 QUnit 测试套件:
module("Templater"); test("_supplant", function() { var templater = new Templater(); equal(templater._supplant("{foo}", {foo: "bar"}), "bar")) equal(templater._supplant("foo {bar}", {bar: "baz"}), "foo baz")); }); test("defineTemplate", function() { var templater = new Templater(); templater.defineTemplate("foo", "{foo}"); equal(template._templates.foo, "{foo}"); }); test("render", function() { var templater = new Templater(); templater.defineTemplate("hello", "hello {world}!"); equal(templater.render("hello", {world: "internet"}), "hello internet!"); }); |
注意代码中对 render 的测试仅仅是一个确保 defineTemplate 和 supplant 能够互相整合的测试。我们已经单独测试了这些方法,从而让我们可以很容易发现 render 的测试失败是具体哪个组件导致的。
编写紧密联系的多个函数
在任何语言中,紧密联系的函数都是重要的,JavaScript 也展示了这么做的原因。你使用 JavaScript 完成的大部分都是由环境提供的全局单例,也是测试套件所依赖的东西。例如,如果你的所有方法都在尝试给 window.location 赋值,那么测试 URL rewriter 就会有困难。与此相反,你应当将系统分解成对应的逻辑组件,决定它们如何去做,并编写实际完成的简短函数。你可以使用多个输入输出测试这些函数逻辑,而不测试那个修改 window.location 的最终函数。这么做既可以正确地组合系统,也能保证安全。
这里是不可测试的 URL rewriter 示例:
function redirectTo(url) { if (url.charAt(0) === "#") { window.location.hash = url; } else if (url.charAt(0) === "/") { window.location.pathname = url; } else { window.location.href = url; } } |
虽然示例中的逻辑很简单,但我们也能设想到情况更复杂的 redirecter 。随着复杂度的上升,我们不能在不触发 window 重定向的情况下测试这个方法,而这样会完全离开测试套件。
这里是可测试版本:
function _getRedirectPart(url) { if (url.charAt(0) === "#") { return "hash"; } else if (url.charAt(0) === "/") { return "pathname"; } else { return "href"; } } function redirectTo(url) { window.location[_getRedirectPart(url)] = url; } |
而现在我们可以为 _getRedirectPart 编写一个简单的测试套件:
test("_getRedirectPart", function() { equal(_getRedirectPart("#foo"), "hash"); equal(_getRedirectPart("/foo"), "pathname"); equal(_getRedirectPart("http://foo.com"), "href"); }); |
现在最重要的 redirectTo 已经通过测试,我们就不必担心会意外地跳转到测试套件之外了。
注意:有一种备选解决方案是创建 `performRedirect` 函数做地址跳转,但是在测试套件中隔离此函数。这是许多人的常用实践,但是我会尽量避免方法隔离。我发现在我目前的所有情形中 QUnit 基本上工作得很好,并且更倾向于像上面那样,不用在测试中隔离函数,但是你的情形可能会不太一样。