AngularJS 的异步服务测试与Mocking
测试 AngularJS 的异步服务
最近,在做项目时掉进了 AngularJS 异步调用 $q 测试的坑中,直接躺枪了。折腾了许久日子,终于想通了其中的道道,但并不确定是最佳的解决方案,最后还是决定总结成文以求能与其它的园友共同分享以求找到更好的解决方案。
首先,我的测试环境是 [Karma|http://karma-runner.github.io/0.12/index.html] + [Jasmine|http://jasmine.github.io/] ,这属于 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
这一次测试 pass 了。既然测试写好了,那么我们就回到最初那里,将实现逻辑真正地加入到 fileReader 服务中:
'use strict'
fileReader=($q)->
(_blob)->
deferred=$q.defer()
reader = new FileReader()
reader.onloadend=(e)->
# 注意:事件触发实质上等同于异步回调
deferred.resolve e.target.result
deferred.promise
angular.module('tdd').service 'fileReader', fileReader
同样地,运行刚才写好的异步测试程序。 当然在运行前要修改一下 expected_text 为 expected_text = chance.sentence()。然而,运行结果是让人失望的:
Chrome 39.0.2171 (Mac OS X 10.10.2) 异步调用测试 应该通过fileReader 服务从Blob 对象中读出文本 FAILED
Expected 'Gipik ejfim renma fanibi nub otu nihwojtat il bepu koufo dibe ohepusaw monumba.' to equal ''.
Error: Expected 'Gipik ejfim renma fanibi nub otu nihwojtat il bepu koufo dibe ohepusaw monumba.' to equal ''.
at Object.<anonymous> (/Users/Ray/code/tdd/test/spec/file_reader.service.spec.js:40:36 <- file_reader.service.spec.coffee:48:28)
at Object.invoke (/Users/Ray/code/tdd/bower_components/angular/angular.js:4182:17)
at Object.workFn (/Users/Ray/code/tdd/bower_components/angular-mocks/angular-mocks.js:2350:20)
很明显,then 方法又无法触发了,deferred.resolve 并没有如我们预期那般在文件读取时调用,而且 \(rootScope.\)apply() 也貌似失效了。在此时真正的问题才开始显现:
当
resolve是在其它的(非Angular Mock)异步调用中返回时$rootScope.$apply()是无法正确触发then的。
我的实际项目比这个要更为复杂,因为我的实际的服务是操控 indexedDB的,众所周知 indexedDB 里一切都是异步的,所以他们在测试中无一通过!
就是为了这个问题我折腾了好久,最好还是将视线落在 ngMock 上。
曙光
[ngMock|https://docs.angularjs.org/api/ngMock] 这个模块上只提供了最简单的三种异步服务 $httpBackend、$tiimeout 和 $interval ,正是因为他们的存在,在我们的测试中可以正常调用 $http 、$resource 等的常规异步服务。 然而, FileAPI, IndexedDB等这些 HTML5内的高等服务并没有提供 mock。当时我的测试初衷并不想mock而是期往能实际地调用,而然这种想法貌似不太容易实现,加之,自我看了 ["Mocking Dependencies AngularJS Tests"|http://www.sitepoint.com/mocking-dependencies-angularjs-tests/] 一文后,更加确定了我的想法。
我的结论是,如果在自定义的 Angular服务中返回的 promise 是在 Angular的 scope内调用 resolve 那么我们直接使用前面第二种测试方式就可以了,但如果 service 是包装了其它的依赖服务,如FileReader 、IndexedDB、WebSQL或其它的以异步方式为主的服务那么就只能通过 mock 来解决测试的问题,要不就不使用 $q而采用 callback 方式将回调方法直接传递给第三方依赖服务(我现在的IndexedDB服务就是这种方式)。
以本文中所提及的 FileReader 为例的话,要测试通过那可以自己写一个 mockFileReader,通过 jasmine 的 spyOn 方法截取方法调用:
beforeEach(function () {
// Mock FileReader
MockFileReader = {
readAsDataURL: function (file) {
if (file === 'file') {
this.result = 'readedFile';
this.onload();
} else if (file === 'progress') {
this.onprogress({total: 70, loaded: 30});
} else {
this.result = 'fileError';
this.onerror();
}
},
readAsText: function (file, encoding) {
if (file === 'file') {
this.result = 'readedFile';
this.onload();
} else if (file === 'progress') {
this.onprogress({total: 70, loaded: 30});
} else {
this.result = 'fileError';
this.onerror();
}
}
};
spyOn(MockFileReader, 'readAsDataURL').and.callThrough();
spyOn(MockFileReader, 'readAsText').and.callThrough();
// 将 MockFileReader 挂到 window 中
$window = {
FileReader: jasmine.createSpy('FileReader').and.returnValue(MockFileReader)
};
笨一点的做法就是对有使用的第三方依赖都编写一个 Mock 加以取代,更快捷的方法就是看看谁已经将这个“轮子”发明了而不用我们重新造一次。
参考:
- [ngMock|https://docs.angularjs.org/api/ngMock]
- [jasmine|http://jasmine.github.io/2.1/introduction.html]
- [Mastering Web Application Development with Angular JS 中文译本 | https://github.com/nexustap/AngularJS/blob/master/ebooks/Mastering-Web-Application-Development-with-AngularJS/3.md]
- [Mocking Dependencies AngularJS Tests|http://www.sitepoint.com/mocking-dependencies-angularjs-tests/]
- [Supplying mocks for services via provider | http://tech.pro/tutorial/1517/supplying-mocks-for-services-via-provide]
- [mockIndexedDB|https://github.com/szimmers/mock-indexeddb]
AngularJS 的异步服务测试与Mocking的更多相关文章
- grpc使用记录(三)简单异步服务实例
目录 grpc使用记录(三)简单异步服务实例 1.编写proto文件,定义服务 2.编译proto文件,生成代码 3.编写服务端代码 async_service.cpp async_service2. ...
- AngularJs之六(服务)
服务:AngularJS 中,服务是一个函数或对象,可在你的 AngularJS 应用中使用.AngularJS 内建了30 多个服务. 最常用的服务:$location 服务, $http 服务 ...
- 让AngularJS的$http 服务像jQuery.ajax()一样工作
让AngularJS的$http 服务像jQuery.ajax()一样工作 $http的post . 请求默认的content-Type=application/json . 提交的是json对象的字 ...
- C# Socket基础(一)之启动异步服务监听
本文主要是以代码为主..NET技术交流群 199281001 .欢迎加入. //通知一个或多个正在等待的线程已发生事件. ManualResetEvent manager = new ManualRe ...
- vs自带服务测试工具
在vs安装目录有一个vs自带的服务测试工具,地址为: "C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\IDE\Wcf ...
- Ⅳ.AngularJS的点点滴滴-- 服务
服务(Angularjs很多方法都是服务组成的) 1.使用service方法创建的单例服务 <html> <script src="http://ajax.googleap ...
- 12个强大的Web服务测试工具
在过去的几年中,web服务或API的普及和使用有所增加. web服务或API是程序或软件组件的集合,可以帮助应用程序进行交互或通过形成其他应用程序或服务器之间的连接执行一些进程/事务处理.基本上有两种 ...
- 怎么理解angularjs中的服务?
AngularJS中的服务其实就是提供一种方式抽取共用类库 比如说一些工具类方法,我们传统的做法就是自己写个 utility 类,把相关的工具方法填充到utility里面去,最后把utility类放到 ...
- AngularJS之使用服务封装
AngularJS之使用服务封装可复用代码 创建服务组件 在AngularJS中创建一个服务组件很简单,只需要定义一个具有$get方法的构造函数, 然后使用模块的provider方法进行登记: / ...
随机推荐
- Prometheus Node_exporter 之 Network Netstat UDP
Network Netstat UDP /proc/net/snmp 1. UDP In / Out type: GraphUnit: shortLabel: Datagrams out (-) / ...
- [SQLServer大对象]——FileTable初体验 (转载)
阅读导航启用FILESTREAM设置更改FILESTRAM设置启用数据库非事务性访问级别FileTable 在我接触FileTable之前,存储文件都是存储文件的链接和扩展名到数据,其实并没有实际的把 ...
- android打开存储卡(TF卡\SD卡)中的sqlite文件
android的SDK直接支持sqlite3的API. 打开SD卡上面的sqlite数据库,不需要SQLiteOpenHelper的继承类.只需要,SQLiteDatabase中的一些静态方法.如 ...
- Access restriction: The type BASE64Encoder is not accessible due to restriction on required library C:\Program Files\Java\jre6\lib\rt.jar
解决方案:在configure build path 中去掉 jre system library,然后重新加载jre system library.....
- 从PFX文件中获取私钥、公钥证书、公钥
https://blog.csdn.net/ZuoYanYouYan/article/details/77868584 该类具体功能:根据pfx证书得到私钥.根据私钥字节数组获取私钥对象.根据公钥字节 ...
- zabbix待完整
fad 下载zabbix3.4的配置文件 wget -O zabbix-3.4.2.tar.gz http://sourceforge.net/projects/zabbix/files/ZABBIX ...
- 【剑指offer】数值的整数次方
版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/mmc_maodun/article/details/25506085 转载请注明出处:http:// ...
- AtCoder Regular Contest
一句话题解 因为上篇AGC的写的有点长……估计这篇也短不了所以放个一句话题解方便查阅啥的吧QwQ 具体的题意代码题解还是往下翻…… ARC 058 D:简单容斥计数. E:用二进制表示放的数字,然后状 ...
- [CQOI2009]叶子的染色
传送门:https://www.luogu.org/problemnew/show/P3155 一道挺水的树形dp题,然后我因为一个挺智障的问题debug了一晚上…… 嗯……首先想,如果一个点的颜色和 ...
- poi 创建excel数据
public static void main(String[] args) throws Exception { // TODO 设置excel的标题 List<String> exce ...