测试 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 之前是采用 runswaitsFor 方法进行处理,在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_textexpected_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 加以取代,更快捷的方法就是看看谁已经将这个“轮子”发明了而不用我们重新造一次。

参考:

AngularJS 的异步服务测试与Mocking的更多相关文章

  1. grpc使用记录(三)简单异步服务实例

    目录 grpc使用记录(三)简单异步服务实例 1.编写proto文件,定义服务 2.编译proto文件,生成代码 3.编写服务端代码 async_service.cpp async_service2. ...

  2. AngularJs之六(服务)

    服务:AngularJS 中,服务是一个函数或对象,可在你的 AngularJS 应用中使用.AngularJS 内建了30 多个服务. 最常用的服务:$location  服务,  $http 服务 ...

  3. 让AngularJS的$http 服务像jQuery.ajax()一样工作

    让AngularJS的$http 服务像jQuery.ajax()一样工作 $http的post . 请求默认的content-Type=application/json . 提交的是json对象的字 ...

  4. C# Socket基础(一)之启动异步服务监听

    本文主要是以代码为主..NET技术交流群 199281001 .欢迎加入. //通知一个或多个正在等待的线程已发生事件. ManualResetEvent manager = new ManualRe ...

  5. vs自带服务测试工具

    在vs安装目录有一个vs自带的服务测试工具,地址为: "C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\IDE\Wcf ...

  6. Ⅳ.AngularJS的点点滴滴-- 服务

    服务(Angularjs很多方法都是服务组成的) 1.使用service方法创建的单例服务 <html> <script src="http://ajax.googleap ...

  7. 12个强大的Web服务测试工具

    在过去的几年中,web服务或API的普及和使用有所增加. web服务或API是程序或软件组件的集合,可以帮助应用程序进行交互或通过形成其他应用程序或服务器之间的连接执行一些进程/事务处理.基本上有两种 ...

  8. 怎么理解angularjs中的服务?

    AngularJS中的服务其实就是提供一种方式抽取共用类库 比如说一些工具类方法,我们传统的做法就是自己写个 utility 类,把相关的工具方法填充到utility里面去,最后把utility类放到 ...

  9. AngularJS之使用服务封装

    AngularJS之使用服务封装可复用代码   创建服务组件 在AngularJS中创建一个服务组件很简单,只需要定义一个具有$get方法的构造函数, 然后使用模块的provider方法进行登记: / ...

随机推荐

  1. 学习笔记:Analyze MySQL Performance及慢日志的开启

    Table of Contents Analyze MySQL PerformanceTuningSlow queries and Slowlog Brought to you by Rick Jam ...

  2. Zabbix添加Ping外网IP监控

    1.先添加一个HOST,只填写IP即可,如10.1.1.1 2.选择一台客户端,新建item,如下: 3.新建trigger: 注:icmpping[<target>,<packet ...

  3. Java语言的主要特点

    Java语言有很多的优点,可靠.安全.编译和解释型语言.分布式.多线程.完全面向对象.与平台无关性等等. 与平台无关性 Java语言最大的优势在于与平台无关性,也就是可以跨平台使用. 绝大多数的编程语 ...

  4. Xmind在win10更改用户设置后出现Invalid Configuration Location 错误的解决办法

    错误原因: 因为一开始新建win10用户时,使用的是中文用户名,导致了部分软件比如IDEA读取C盘中配置文件时报错.我用管理员权限修改用户姓名为英文后,IDEA的问题虽然已经解决,但Xmind却报出了 ...

  5. 6.基本数据结构-双端队列(Deque)

    一.双端队列(Deque) - 概念:deque(也称为双端队列)是与队列类似的项的有序集合.它有两个端部,首部和尾部,并且项在集合中保持不变. - 特性:deque 特殊之处在于添加和删除项是非限制 ...

  6. Breaking Down Type Erasure in Swift

    Type Erasure Pattern We can use the type erasure pattern to combine both generic type parameters and ...

  7. Nowcoder 提高组练习赛-R3

    https://www.nowcoder.com/acm/contest/174#question 今天的题好难呀,只有94个人有分.然后我就爆零光荣 考到一半发现我们班要上物理课,还要去做物理实验( ...

  8. loli的搜索测试-我真不知道是第多少次了

    搜索测试 又到了....并不激动人心的搜索测试时间. 今天和以前还是有一点不一样的,新高二的学长们也参加了(也就是说我们又要被吊打了) 话不多说,看题: fz:填一个5*5的质数方阵,要求每行,每列, ...

  9. ES6标准入门之变量的解构赋值简单解说

    首先我们来看一看解构的概念,在ES6标准下,允许按照一定模式从数组和对象中提取值,然后对变量进行赋值,这被称作解构,简而言之粗糙的理解就是变相赋值. 解构赋值的规则是,只要等号右边的值不是对象或者数组 ...

  10. ethjs-1-了解

    https://github.com/ethjs/ethjs/blob/master/docs/user-guide.md Install npm install --save ethjs Usage ...