《AngularJS深度剖析与最佳实践》一2.12 单元测试-阿里云开发者社区

开发者社区> 华章出版社> 正文
登录阅读全文

《AngularJS深度剖析与最佳实践》一2.12 单元测试

简介:

本节书摘来自华章出版社《AngularJS深度剖析与最佳实践》一书中的第2章,第2.12节,作者 雪狼 破狼 彭洪伟,更多章节内容可以访问云栖社区“华章计算机”公众号查看

2.12 单元测试

我们在第1章中已经写过两个单元测试(unit test)了,这里我们简单讲一下理论知识。
在Angular中,单元测试的概念和传统的后端编程是一样的。也就是对某些小型功能块儿进行测试,保障其工作逻辑正常。单元测试要尽可能局部化,不要牵扯进很多个模块,必要时可进行mock(模拟)。

2.12.1 MOCK的使用方式

由于JavaScript语言的动态特性,Mock一个普通对象不需要进行特别处理。比如,如果一个测试函数需要访问scope中的一个变量:name,但不用访问$watch等scope的特有函数,那么传入一个普通的哈希对象{name: 'someName'}即可,并不需要new出一个scope来。
除了局部化以外,对单元测试来说,一大挑战是网络操作,如果使用真实的网络操作,那么将带来几个问题:
网络的不稳定性,导致单元测试的不稳定性。想象一个有时成功,有时失败的单元测试,会让程序员多么头疼吧!
网络响应的速度会拖慢整体速度。单元测试执行得必须尽可能快速,如果被迫由于网络操作而变慢,那么一旦多了就会变得很慢,也就会有很多时间浪费在这里。
网络的异步性。虽然异步调用对于单元测试来说并不是不可接受的,但是由于其返回时机不受控制,所以写起来还是比同步调用复杂一些。
另一大挑战是与时间有关的测试。比如一段代码中设置了一小时后触发的定时器,难道我们的单元测试就要一个小时后才能完成?这显然是不合理的。
好在,Angular对网络和定时器等进行了封装,变成了$http、$timeout、$interval等服务。这就意味着,我们只要使用这些内置服务而不是setTimeout等原生函数,那么我们就可以对它们进行Mock,克服上述问题。
对于这些内置服务,Angular提供了一个独立的测试库:angular-mocks.js。
它对Angular的一些内置服务进行了Mock,比如$httpBackend、$timeout、$interval、$exceptionHandler、$log等服务。还提供了一些工具函数,如用于加载模块的module函数、用于依赖注入的inject函数、用于调试的dump函数等,这些函数都是顶层函数,不需要加前缀就可以调用。
但Angular实际上没有Mock $http服务,而是Mock了XHR(XMLHttpRequest)对象,它把原来发送到服务端的Ajax调用,转变成本地调用。这个通过本地调用来模拟服务器的对象则是$httpBackend(模拟http后端,也就是服务器)。

$httpBackend.whenGET('/someUrl').respond({name: 'wolf'}, {'X-Record-Count': 100});

上述语句声明了一个模拟服务端,当被测试代码请求GET /someUrl这个地址时,将被$httpBackend拦截,并返回一个JSON对象{"name": "wolf"},同时,返回一个额外的response header:X-Record-Count,其值为"100"。
注意,我们这里其实只是定义了返回规则,并没有规定啥时候返回这些数据,也就是说,虽然被测代码中的$http函数已经能正确返回我们期望的数据,但目前还不会被触发—直到我们调用了$httpBackend.flush函数。这样,我们就把测试中的异步调用变成了同步调用。
respond中的参数不但可以是一个或两个哈希对象,还可以在前面增加一个返回码,如respond(401, {message: 'Unauthorized'}, {'X-Sign-It': '1887a6b'})等,Angular会自动判断它的数据类型,来决定使用哪种重载形式。如果你需要更多的控制力,还可以转而传入一个函数,其原型是:function(method, url, data, headers) {},这个函数中的四个参数都是由$http请求发来的数据,这个函数的返回值是一个数组,包含状态码、数据等信息,完整的范例如:

$httpBackend.whenPOST('/someUrl').respond(function(method, url, data, headers) {
    var result = 'Hello, ' + data.name;
    return [201, result, {'X-Greeting': 'Say Hello'}, 'OK'];
});

这样,当被测代码请求调用$http.post('/someUrl', {name: 'wolf'}),然后调用$httpBackend. flush()时,获得的回应为:状态码201,回应体:Hello, wolf,回应头:X-Greeting: 'Say Hello',同时它的status text为OK。
不过,虽然这种形式很灵活,但对于单元测试来说,还是不应该把mock逻辑写得过于复杂,否则,如果测试代码本身都可能出错,会让你的测试变得非常痛苦。写mock时,推荐的最佳实践是“给出固定数据,返回固定数据”。
如果把上述代码改写为:$httpBackend.whenPOST('/someUrl', {name: 'wolf'}).respond ('Hello, wolf', {'X-Greeting': 'Say Hello'}, 'OK');,不但代码量少了很多,而且更加简洁明确,更能发挥“测试”作为“规约”的作用。
$timeout和$interval的Mock就比较简单了,只是增加了一个flush函数,它的作用和$httpBackend.flush一样,也是立即触发这个异步操作。

2.12.2 测试工具与断言库

我们写好了测试,还要把它跑起来,用来跑测试的工具称为Test Runner,Angular的范例工程中集成的测试工具是Karma,它的用法对写测试来说几乎可以不用管。
而代码中用来写断言的库称为断言库,在范例工程中集成的是jasmine。我们测试代码中的expect和toBe等函数都是来自它的。具体的使用方式可以参见它们的官方文档,此处就不展开讲解了。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:

华章出版社

官方博客
官网链接