本节书摘来自华章出版社《angularjs深度剖析与最佳实践》一书中的第2章,第2.12节,作者 雪狼 破狼 彭洪伟,更多章节内容可以访问云栖社区“华章计算机”公众号查看
我们在第1章中已经写过两个单元测试(unit test)了,这里我们简单讲一下理论知识。
在angular中,单元测试的概念和传统的后端编程是一样的。也就是对某些小型功能块儿进行测试,保障其工作逻辑正常。单元测试要尽可能局部化,不要牵扯进很多个模块,必要时可进行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后端,也就是服务器)。
上述语句声明了一个模拟服务端,当被测试代码请求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请求发来的数据,这个函数的返回值是一个数组,包含状态码、数据等信息,完整的范例如:
这样,当被测代码请求调用$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一样,也是立即触发这个异步操作。
我们写好了测试,还要把它跑起来,用来跑测试的工具称为test runner,angular的范例工程中集成的测试工具是karma,它的用法对写测试来说几乎可以不用管。
而代码中用来写断言的库称为断言库,在范例工程中集成的是jasmine。我们测试代码中的expect和tobe等函数都是来自它的。具体的使用方式可以参见它们的官方文档,此处就不展开讲解了。