测试 AngularJS 的异步服务
最近,在做项目时掉进了 AngularJS 异步调用 $q 测试的坑中,直接躺枪了。折腾了许久日子,终于想通了其中的道道,但并不确定是最佳的解决方案,最后还是决定总结成文以求能与其它的园友共同分享以求找到更好的解决方案。
首先,我的测试环境是 [Karma|] + [Jasmine|] ,这属于 AngularJS的其中一种配置,也是AngularJS官方所推荐的框架,Jasmine 用起来也确实很不错。
很多的spec都没有什么大问题,只是当我为其中的几重要的异步处理服务编写测试时就出事了,代码在实际运行环境中是能正常运行的,但在测试中却不能通过,这肯定是测试没写好。在网上google了多天,也对各种方案进行尝试一直也没有找到解决方法,然而这种问题不会是特例,而是经常会遇到的,那就是在Angular服务中返回的 promise 很难进行测试。
从代码入手会更容易了解问题的始末:
噩梦的开始jasmine 的异步测试模式是实现一种简单的超时机制,通过等待 done() 方法对计时器重置,当在超时限制内(默认5s) done() 没有被调用则会引发测试失败的异常。在 1.3 之前是采用 runs 和 waitsFor 方法进行处理,在2.0后这两个方法被简化去除掉了,只能用 done,这里就以我们最经常会用到的 FileAPI 中的 FileReader 来做实例,FileReader 对文件对象(Blob)的读取是一个异步方法,那么将这个实现逻辑直接写在 jasmine 中应该是这样的:
describe '异步调用测试', -> beforeEach module 'tdd' it 'Blob内的数据应该被读取为文本', (done)-> expected_text = chance.sentence() # 用 chance 产生随机的字符串 blob = new Blob([expected_text]) reader = new FileReader() reader.onloadend = (e)-> expect(e.target.result).toBe expected_text done() reader.readAsText blob测试结果是 pass , 这只是为了试用一下 jasmine 中 done 的效果。当然在项目中这样做是完全没有意义的,这只是一个引子,我会分三步来完整这个测试。
接下来是将这个实现逻辑封装成为 AngularJS的 service。由于是异步处理所以这个 service 应该是返回一个 promise 对象。 为了更具体地说明这个问题,这里只建立一个空白的 fileReader 服务,此服务只为了测试 then 的触发时机:
'use strict' fileReader=($q)-> (_blob)-> deferred=$q.defer() deferred.resolve('马上返回') deferred.promise angular.module('tdd').service 'fileReader', fileReader那么前文的测试就应该修改为:
describe '异步调用测试' ,-> beforeEach module 'tdd' _fileReader={} it '应该通过fileReader 服务从Blob 对象中读出文本', (done)-> expected_text = '马上返回' blob = new Blob([expected_text]) fileReader(blob).then (actual_text)-> expect(expected_text).toEqual actual_text done()问题开始来了,这个测试运行的结果是 Fail! 并且得到以下的提示:
Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.超时!也就是说 由于then 并没有被调用,所以超时返回方法 done()没有被执行而直接出现这个测试错误。然而讽刺的是,fileReader 这个服务在浏览器内是可以直接运行而不会产生任何的错误的。
问题出在哪里 ?找了许久,最终发现,由于在 jasmine 中的环境是被 mock 出来的,是由 ngMock 对 angular 的对象和brower对象内的服务进行了重新的模拟,这个会与实际的运行有些许的差异,由其要使 then 方法被正确调用那需要在返回then之后调用 $rootScope.$apply() (这个内容可以直接参考:https://docs.angularjs.org/api/ng/service/$q) 也就是说,我们并不需要直接使用 jasmine 提供的“阻塞”模拟,而是直接用 $rootScope.$apply() 让异步方法直接返回。
describe ' 异步调用测试' , -> describe module 'tdd' it '应该通过fileReader 服务从Blob 对象中读出文本',$inject (fileReader,$rootScope)-> expected_text='马上返回' blob=new Blob([expected_text]) actual_text='' fileReader(blob).then (data)-> actual=data $rootScope.$apply() expect(expected_text) .toEqual actual_text