AngularJSAngularJS 的异步服务测试与Mocking

测试 AngularJS 的异步服务

近日,在做项目时掉进了 AngularJS 异步调用 $q
测试的坑中,直接躺枪了。折腾了许久日子,终于想通了其中的道道,但并不确定是顶级的化解方案,最终照旧控制总括成文以求能与其他的园友共同享受以求找到更好的解决方案。

先是,我的测试环境是
[Karma|http://karma-runner.github.io/0.12/index.html] +
[Jasmine|http://jasmine.github.io/] ,那属于
AngularJS的里边一种配备,也是AngularJS官方所推荐的框架,贾斯敏(Jasmine)(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
加以取代,更快捷的办法就是看看什么人已经将以此“轮子”发明了而不用大家再一次造四回。

参考:

相关文章