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 之前是使 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_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]

相关文章