这一次测试 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"|] 一文后,更加确定了我的想法。
我的结论是,如果在自定义的 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) };