JavaScript是一门有强大表现力的动态类型语言,但它几乎无法从compiler中得到任何帮助。因此我们强调任何用JavaScript写的代码都需要有一组测试方法。我们在Angular中添加很多特性让你测试程序更方便,所以没有理由不写测试。
单元测试(Unit Test),如同它的名字所说,是关于单独一块代码的测试。单元测试试图回答这个问题:我认为我的业务逻辑是正确的吗?比如,这个排序函数是否将list按正确的顺序进行了排序?为了能回答这样的问题,隔离代码很重要。因为当我们测试这种函数时,我们不想被强迫去考虑那些如DOM操作、使用XHR取数据等无关操作。
在一个项目中,我们通常很难只调用一个单独的函数。原因在于开发者们经常把关注者混合在了一起,以致于一块代码中把所有的事情都干了:从XHR中读取数据,排序,然后操作DOM。在Angular中我们尝试让你以简单的方式做正确的事情,所以我们为你的XHR提供了依赖注入(在测试中你可以使用mock)。我们进行了抽象,让你对model进行排序时,无需手动对DOM进行排序操作。所以最终,测试一个“对数据排序”的函数变得容易,因为我们可以在测试中简单地创建一些数据集,应用到该函数,最后验证model中的数据的顺序是正确的。测试代码不需要等待XHR,不需要创建任何DOM,也不需要验证你的函数正确地修改了DOM。
Angular始终把“可测试性”放在重要位置,但仍然需要你做正确的事情。我们试着让正确的事情变得简单,但Angular不是魔法,如果你没有注意这些,最后可能还是写出了无法测试的代码。
有几种方法可以取得一个依赖:
new
操作符创建2. 从一个叫“全局单例”的地方寻找3. 从注册表(如service注册表)中寻找(但你从哪儿找到这个注册表呢?参看2)4. 你也可以期待有人把它递给你在上面的例子中,只有最后一个是可测试的。让我们看看为什么:
new
操作符虽然从根本上讲new
操作符没有错,但问题是使用对一个构造器使用new
操作符,等于永久性地将类型与调用位置写死了。例如我们准备初始化一个XHR
用来从server中取数据:
function MyClass() {
this.doWork = function() {
var xhr = new XHR();
xhr.open(method, url, true);
xhr.onreadystatechange = function() {...}
xhr.send();
}
}
问题在于,在测试中,我们非常希望初始化一个`MockXHR`来返回假数据和模板网络错误。而使用`new XHR()`,我们总是创建了一个真实的XHR,没有好办法来替换它。没错,虽然我们有`monkey patching`,但那从很多方面讲都是一个坏主意,当然这已经超出了本文的范围。
上面的这个类很难测试,因为我们必须求助于monkey patching:
var oldXHR = XHR;
XHR = function MockXHR() {};
var myClass = new MyClass();
myClass.doWork();
// assert that MockXHR got called with the right arguments
XHR = oldXHR; // if you forget this bad things will happen
##### 全局查找
另一种方法是在一个知名位置(全局单例)寻找service。
function MyClass() {
this.doWork = function() {
global.xhr({
method:'...',
url:'...',
complete:function(response){ ... }
})
}
}
由于没有创建依赖的实例,从基本上讲它跟`new`是一样的,在测试中没有好办法拦截对`global.xhr`的调用,除非使用mongo patching。使用全局变量的基本问题是这些全局变量必须是可变的,可以被mock方法替换。关于更进一步的解释,参看[Brittle Global State & Singletons](http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons)。
上面的代码之所以难以被测试,是因为我们必须改变全局状态:
var oldXHR = global.xhr;
global.xhr = function mockXHR() {};
var myClass = new MyClass();
myClass.doWork();
// assert that mockXHR got called with the right arguments
global.xhr = oldXHR; // if you forget this bad things will happen
##### Service注册表
看起来我们可以对所有的services使用一个注册表,然后在测试中替换需要的service。
function MyClass() {
var serviceRegistry = ????;
this.doWork = function() {
var xhr = serviceRegistry.get('xhr');
xhr({
method:'...',
url:'...',
complete:function(response){ ... }
})
}
问题在于,我们从哪里去取得这个`serviceRegistry`?如果它是:
new
出来的,则测试没有机会在测试中重设services* 从全局位置中找到的,则返回来的这个service也是全局的(虽然重设容易些,因为仅有一个全局变量需要重设)
上面的类难以测试,是因为我们必须改变全局变量:
var oldServiceLocator = global.serviceLocator;
global.serviceLocator.set('xhr', function mockXHR() {});
var myClass = new MyClass();
myClass.doWork();
// assert that mockXHR got called with the right arguments
global.serviceLocator = oldServiceLocator; // if you forget this bad things will happen
最后,如果依赖可以被传入:
function MyClass(xhr) {
this.doWork = function() {
xhr({
method:'...',
url:'...',
complete:function(response){ ... }
})
}
这是更好的做法,因为我们不需要假设xhr
是从哪儿来的,因为谁创建了这个类谁就有责任传一个xhr进来。由于该类的创建者与使用者不同,所以就从业务逻辑中把创建的责任分离出来了,简单地说,这就是依赖注入。
上面的类很好测试,因为在测试中我们可以:
function xhrMock(args) {…}
var myClass = new MyClass(xhrMock);
myClass.doWork();
// assert that xhrMock got called with the right arguments
注意在写这个测试过程中,没有出现担心的全局变量
Angular内置了依赖注入,让正确的事情容易做,但你还是得去做(测试)。
让程序变得与众不同的是它的业务逻辑,这也是我们想测试的东西。如果你的业务逻辑跟DOM操作混合在一起,则它会像下例一样难测:
function PasswordController() {
// get references to DOM elements
var msg = $('.ex1 span');
var input = $('.ex1 input');
var strength;
this.grade = function() {
msg.removeClass(strength);
var pwd = input.val();
password.text(pwd);
if (pwd.length > 8) {
strength = 'strong';
} else if (pwd.length > 3) {
strength = 'medium';
} else {
strength = 'weak';
}
msg
.addClass(strength)
.text(strength);
}
}
上面的代码难以测试,因为它需要你的测试代码运行时,有正确的DOM供使用。所以测试代码看起来会是这样:
var input = $('');
var pc = new PasswordController();
在Angular中controller的业务逻辑与DOM操作严格分离,所以测试起来容易多了,见下例: function PasswordCntrl($scope) {
};
测试代码也很直白: var pc = new PasswordController();
注意,这个测试不但简短很多,而且很容易看明白它在测什么。我们说这样的一个测试在讲一个故事,而不需要去验证一些不相关的东西。 myModule.filter('length', function() {
}
var length = $filter('length');
Angular中的Directives在model发生变化时,有责任更新DOM。(这段没写完) (这段只有标题)
var span = $('');
$('body').html('
.find('div')
.append(input)
.append(span);
input.val('abc');
pc.grade();
expect(span.text()).toEqual('weak');
$('body').html('');
$scope.password = '';
$scope.grade = function() {var size = $scope.password.length;
if (size > 8) {
$scope.strength = 'strong';
} else if (size > 3) {
$scope.strength = 'medium';
} else {
$scope.strength = 'weak';
}
}
pc.password('abc');
pc.grade();
expect(span.strength).toEqual('weak');FIlters
Filters
是一些把数据转换为用户易读的格式的函数。它们很重要,因为它们把格式化数据的功能从程序逻辑中独立出来了,大大简化了程序逻辑。
return function(text){return (''+(text||'')).length;
});
expect(length(null)).toEqual(0);
expect(length('abc')).toEqual(3);Directives