Angular JS - unit test

Edit


前端代码一直没有做unit testing,也不知道怎么做。在Yaakov Chaikin的Coursera课程中,第五章正好介绍了相关内容。所以就顺便在自己的项目中实践了一把。无数次的实践证明,看得头头是道,写起来就到处是坑。小小的内容,着实花了不少时间。

针对前端的代码,网上采用的测试框架大多是karma + jasmine + chai。因为我之前在后端代码的测试中多采用mocha + should + sinon。所以本次实践,仍然爱采用mocha框架,来搭配karma使用。

karma是一个自动化测试框架。因其丰富的配置选项,良好的扩展性能,被广泛应用,尤其是在前端。因为前端要针对各种版本的浏览器,各种框架,如果全部靠手动去适配,基本不太可能。所以大家都用karma了。

karma setup

基本配置参考前端单元测试之Karma环境搭建

配置文件简单如下:

module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: [...],
// list of files / patterns to load in the browser
files: [
'...',
{pattern: '...'}
],
// list of files to exclude
exclude: [
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['...'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: false,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity,
// Which plugins to enable
plugins: []
})
}

简单的解释目前用到的选项:

  • frameworks: 使用到的框架,所有使用到的,要在下面plugins中列明。比如:frameworks中包含mocha,则plugins下就要包含karma-mocha
  • basePath, files, exclude: 这三个指明了karma要加载的JS文件
    • basePath:基础目录,会直接插入到files,exclude指定的路径前面
    • files:加载文件,支持通配符和正则式,通过大括号还可以添加配置选项。通配符’**’表示所有目录,’*’表示所有文件,’**/*’表示其下所有目录所有文件。
    • exclude: 可以剔除files中不想要的文件
  • browsers:使用的浏览器,如果想要headless测试则可用PhantomJS

karma with ES6

因为PhantomJS不支持ES6语法(网上说,PhantomJS甚至对ES5的支持都不是很好),所以karma要测试包含ES6语法的代码,还需要一番配置。网上相关的帖子很多,列举如下:

总的来说,有这么几种解决方案:

  • karma-es6-shim
  • karma-babel-preprocessor
  • webpack
  • SlimerJS
    前三种有点复杂,目前的我的技术栈还无法理解。最终选择最简单的SlimerJS。在Linux上可以通过Xvfb做到headless测试,Windows上就这样吧,跳个窗口也不是什么大事。

Debug karma test using Chrome

这个在初学的时候还是很重要的,如果没有这个工具,恐怕我也跳不出自己给自己挖的大坑。
参考链接:Debugging Karma Unit Tests
按照上面的步骤打开调试页面和Chrome Dev Tool页面,刷新页面即可重新加载代码并触发断点。
将命令行写入package.json,以后调用npm就可以方便的打开该调试工具。

{
...
"scripts": {
"start": "node ./bin/www",
"karma-debug": "cd public/js && karma start --browsers=Chrome --single-run=false --debug"
},
...
}

ngMock

ngMock中两个最重要的关键词就是:module, inject。可以参考这个人的博客:

ngMock通过module加载已经定义了的模块,或者定义匿名module并提供新的service,或者复写已存在的service。module只是将名字,以及对应位置存入一个数组。inject将真正的加载所有module提供的service,并解决依赖关系,还可以将相应的service导出到test case中使用。其实ngMock也就是做了这两件事情。
下面简单介绍一些有助于理解的知识点。

angular.mock.module

module函数的原型是angular.mock.module(alias, obj, func)
module接受三种参数类型,每一种都可以单独使用,用来定义或加载对应模块

  • alias: 是module名字字符串,可以是在待测代码中定义的,也可以是在测试文件中定义的
  • obj: 一个普通的字典结构,他会生成一个匿名module,其中的成员会被定义成$provide.value()
  • function: 同样是定义一个匿名module,不同的是,function可以用$provide作为参数,来定义各种各样的service。

function() vs Object

The examples we used were similar, so what’s the key difference between the function() and Object form? As the Object type gets added as a $provider.value, it is therefore restricted to the capabilities of the value service, most importantly that a value service cannot be injected with other services.

关于$provide,可以参考Angular官方文档

The $provide service has a number of methods for registering components with the $injector. Many of these functions are also exposed on angular.Module.

它提供了下面的这些service

provider(name, provider) - registers a service provider with the $injector
constant(name, obj) - registers a value/object that can be accessed by providers and services.
value(name, obj) - registers a value/object that can only be accessed by services, not providers.
factory(name, fn) - registers a service factory function that will be wrapped in a service provider object, whose $get property will contain the given factory function.
service(name, Fn) - registers a constructor function that will be wrapped in a service provider object, whose $get property will instantiate a new object using the given constructor function.
decorator(name, decorFn) - registers a decorator function that will be able to modify or replace the implementation of another service.

angular.mock.inject

$inject是ngMock的核心,看看官方的定义

The inject function wraps a function into an injectable function. The inject() creates new instance of $injector per test, which is then used for resolving references.

inject就是定义了一个injector,那injector又是什么

$injector is used to retrieve object instances as defined by provider, instantiate types, invoke methods, and load modules.

$injector也就是AngularJS的核心。它提供了所谓的“DI” (Dependency Injection)模式。Angular的核心代码通过$injector来获取对应service的reference。
我们通常将inject放在beforeEach里,这样inject就为每个测试定义一个injector,为这个injector准备好所有的service,并能通过参数将任意已有的service导出。

Test controllers

了解了上面的两个重要组件之后,针对不同的测试组件就容易理解了。
直接上代码:

describe('main controller', function() {
var $controller
var mainCtrl
beforeEach('set up module', function() {
module(function ($provide) {
$provide.service('MockDataService', function () {
const service = this
service.data = 'test data'
})
})
module('MainModule')
});
beforeEach('set up controller', inject(function (_$controller_, MockDataService) {
$controller = _$controller_
mainCtrl = $controller('MainController', {
DataService: MockDataService
})
}))
it('should assemble print page URL correctly', function() {
mainCtrl.getData().should.equal('test data')
});
});

这里只有两个值得注意的地方,其实前文也都有提及。

  1. 第一个module中通过$provide提供了一个假的service,这个service可以通过其名字被inject。
  2. inject中通过_$controller_来导出controller的构造函数。通过这个构造函数来构造相应的待测controller,并传入假的service达到mock的目的。

Test services

仍然直接上代码:

angular.module('PrinterData', [])
.config(['$locationProvider', function($locationProvider) {
$locationProvider.html5Mode(true);
}]);
describe('printer data service', function() {
var $httpBackend
var PrinterDataService
beforeEach(function(){
module(function ($provide) {
$provide.provider('$location', function () {
const provider = this
const location = {}
location.search = function (){
return {
id: 'test id',
}
}
provider.html5Mode = sinon.stub()
provider.$get = sinon.stub().returns(location)
})
})
});
beforeEach(function() {
module('Data')
inject(function ($injector) {
$httpBackend = $injector.get('$httpBackend')
DataService = $injector.get('DataService')
})
});
it('should update if succeeds', function(done) {
$httpBackend.whenGET(/.*/)
.respond({
status: 'ready'
})
DataService.getStatus().then(function (data) {
data.should.deepEqual({
status: 'ready'
})
done()
}).catch(function (err) {
should.not.exist(err)
})
$httpBackend.flush();
});
})

下面罗列一些实际使用中碰到的一些问题,以及自己的一些理解。

使用$injector

我们说过inject函数就是用来创建$injector,所以$injector也是可以直接被导出使用的。这时需要什么service,就可以直接调用$injector.get。为什么不直接通过参数来inject呢?当然是因为参数太长了,不好看。。。

mock $http

为了测试稳定性,我们在unit test中并不真正的访问服务器,而通常采用假的服务器响应来mock。这就需要用到$httpBackend service。那么$http和$httpBackend之间的关系是什么?

HTTP backend used by the service that delegates to XMLHttpRequest object or JSONP and deals with browser incompatibilities.
You should never need to use this service directly, instead use the higher-level abstractions: $http or $resource.
During testing this implementation is swapped with mock $httpBackend which can be trained with responses.

具体做法,上面的代码中已有,节录如下:

beforeEach(function() {
module('Data')
inject(function ($injector) {
$httpBackend = $injector.get('$httpBackend')
DataService = $injector.get('DataService')
})
});
it('should update if succeeds', function(done) {
$httpBackend.whenGET(/.*/)
.respond({
status: 'ready'
})
DataService.getStatus().then(function (data) {
data.should.deepEqual({
status: 'ready'
})
done()
}).catch(function (err) {
should.not.exist(err)
})
$httpBackend.flush();
});

通过$httpBackend.wheGET(...).respond(...)来设置mock response。whenGET还支持正则式。
调用完请求函数后要记得$httpBackend.flush()将请求真正的发出。否则请求并不会发出,直到调用flush()函数,或者测试超时失败。

如何mock $location & $locationProvider

很惭愧的说,在这个问题上我花了很久很久的时间,搜索了大量的google网页,最后还是依靠chrome追踪angular核心代码才发现原来是自己给自己挖的坑,对service, factory和provider理解不透彻。
首先来就事论事,说一下到底怎么完成我的这个测试。

因为我要测试DataService,而DataService定义在Data moduleDataService调用了$location service。需要将之mock掉。最简单的想法就是通过inject来获得$location service reference。并通过sinon.stub来设置需要的返回值。

inject(function ($injector, $location) {
sinon.stub($location, 'search').returns({
id: 'test id'
})
DataService = $injector.get('DataService')
})

但是我倒霉催的,竟然是用$provide做了:

module(function ($provide) {
$provide.service('$location', function () {
const location = this
location.search = sinon.stub().returns({
device_id: 'test id',
})
})
})

这样就会有问题了,当ngMock加载这个模块的时候,会先定义一个$locationProvider,这就会覆盖其自带的$locationProvider。而如下的module.config方法中对$locationProvider的操作就会出现问题:

angular.module('PrinterData', [])
.config(['$locationProvider', function($locationProvider) {
$locationProvider.html5Mode(true);
}]);

因为新定义的$locationProvider中没有html5Mode成员。于是想了各种办法来mock $locationProvider。结果因为对service,factory,provider的理解不正确,都失败了。放一段错误的代码:

beforeEach(function(){
module(function ($provide) {
$provide.service('$locationProvider', function () {
const location = this
location.html5Mode = function(){}
})
$provide.factory('$location', function () {
const locationProvider = this
locationProvider.html5Mode = function(){}
})
})

第一个用$provide.service会定义一个$locationProviderProvider
第二个用$provide.factory定义了一个$locationProvider,但是没有返回,所以$location service不会被实例化,也就没有$location service了。
如果想通过mock $locationProvider来mock $location service的话,正确的做法应当是:

$provide.provider('$location', function () {
const provider = this
const location = {}
location.search = function (){
return {
device_id: 'test id'
}
}
provider.html5Mode = sinon.stub()
provider.$get = sinon.stub().returns(location)
})

mock掉provider.$get函数,提供假的location服务。其实还有很多办法,例如获得$locationProvider再对$get函数进行stub,也可以。没有再一一尝试了。理解了错误和正确的做法,想再去实现就得心应手了。

测试.config/.run块

Data module有一个.config块,因为模块加载的时候会先运行其.config块,然后是.run块。因其特殊性,所以在对其中使用到的服务进行mock要注意顺序。可以参考这个链接:Testing config and run blocks in AngularJS
How to easily test an often neglected part of your application

如何mock $interval

在单元测试中,我们通常要采用mock的手段,而不真正的使用时间服务。在sinon中,可以使用fake timer来取代JS中标准的setTimeout和setInterval。不过这个在Angular JS中并不起作用,如果我们使用$interval服务的话。
因为在对Angular JS做单元测试的时候,$interval并不会调用setInterval。原因是这里的$interval其实是ngMock中的$interval。此时可以使用$interval.flush(ms)来将时钟推进若干时间。当然如果愿意,也可以用sinon来mock从$injector中get出来的$interval service。$timeout service也类似,有同样的方法对付单元测试。

Promise not resolve

在Angular JS中,我们往往调用$q service来使用Promise,而$q.defer().promise只在$scope的digest cycle才会判定resolve还是reject,所以要触发$q产生的promise,一定要调用对应的$scope.$apply()或者$scope.$digest()。stack overflow中也有大把类似问题的解答。具体也可以参考这篇博客,讲得很透彻:

在Angular JS中,也可以使用标准的Promise,例如: var promise = new Promise(resolve, reject)。但是貌似,标准的Promise和\q兼容的并不很好,下面的code并不会工作:

var promise = $q.all([promise1, promise2])
$scope.$apply()

promise并不会在promise1和promise2返回之后返回,实际上是他根本就不会返回,直到测试超时失败。

“Unexpected request: GET …/.html” on Karma tests

这是Yaakov在视频中提到的需要使用$templateCache的情形。Yaakov提到用karma是解决该问题最好的办法,但是因为视频并不涉及介绍karma,所以并没有说应该怎么做。
当我们的karma test加载了route.js,并在其中定义了component,则测试在实例化该Angular component的时候回去用$http service来GET该template html文件,于是产生了该unexpected request错误。Yaakov在教程中的解决办法是,通过一次真正的AJAX从服务器上请求该html,并将之存入$templateCache中,之后就不会再有真正的GET AJAX产生了。其实这一段可以通过配置karma来实现。具体做法如下:

  • 安装ng-html2js,karma-ng-html2js-preprocessor
  • 使用ng-html2js将对应的template html转成js模块,并在测试代码中引入。

看下面具体代码
karma.conf.js

// list of files / patterns to load in the browser
files: [
'../html/templates/*.html',
...
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'../html/templates/home.template.html': ['ng-html2js']
},
ngHtml2JsPreprocessor: {
moduleName: 'templates',
stripPrefix: '.*/',
prependPrefix: '/html/templates/'
},

test.js

describe(function(){
beforeEach(module('templates'))
})

特别要注意的是对ng-html2js的配置

ngHtml2JsPreprocessor: {
moduleName: 'templates',
stripPrefix: '.*/',
prependPrefix: '/html/templates/'
},

当没有stripPrefix选项时,通过Chrome debug可以看到,所有的template都被转化成了js模块,但是其模块名是类似c:\path\to\template\template.html的结构,而我在route中的指定路径是web路径templateUrl: '/html/templates/home.template.html'。就对不上号了。做法是通过stripPrefix将所有的前缀全部剥去,然后用prependPrefix加上需要的前缀。这样就可以工作了。网上有很多帖子讨论这个问题。列两个有启发性的:

%23%20Angular%20JS%20-%20unit%20test%0A@%28myblog%29%5Bangular%2C%20unittest%5D%0A%u524D%u7AEF%u4EE3%u7801%u4E00%u76F4%u6CA1%u6709%u505Aunit%20testing%uFF0C%u4E5F%u4E0D%u77E5%u9053%u600E%u4E48%u505A%u3002%u5728Yaakov%20Chaikin%u7684%5BCoursera%u8BFE%u7A0B%5D%28https%3A//www.coursera.org/learn/single-page-web-apps-with-angularjs/home/week/5%29%u4E2D%uFF0C%u7B2C%u4E94%u7AE0%u6B63%u597D%u4ECB%u7ECD%u4E86%u76F8%u5173%u5185%u5BB9%u3002%u6240%u4EE5%u5C31%u987A%u4FBF%u5728%u81EA%u5DF1%u7684%u9879%u76EE%u4E2D%u5B9E%u8DF5%u4E86%u4E00%u628A%u3002%u65E0%u6570%u6B21%u7684%u5B9E%u8DF5%u8BC1%u660E%uFF0C%u770B%u5F97%u5934%u5934%u662F%u9053%uFF0C%u5199%u8D77%u6765%u5C31%u5230%u5904%u662F%u5751%u3002%u5C0F%u5C0F%u7684%u5185%u5BB9%uFF0C%u7740%u5B9E%u82B1%u4E86%u4E0D%u5C11%u65F6%u95F4%u3002%0A%0A%u9488%u5BF9%u524D%u7AEF%u7684%u4EE3%u7801%uFF0C%u7F51%u4E0A%u91C7%u7528%u7684%u6D4B%u8BD5%u6846%u67B6%u5927%u591A%u662Fkarma%20+%20jasmine%20+%20chai%u3002%u56E0%u4E3A%u6211%u4E4B%u524D%u5728%u540E%u7AEF%u4EE3%u7801%u7684%u6D4B%u8BD5%u4E2D%u591A%u91C7%u7528mocha%20+%20should%20+%20sinon%u3002%u6240%u4EE5%u672C%u6B21%u5B9E%u8DF5%uFF0C%u4ECD%u7136%u7231%u91C7%u7528mocha%u6846%u67B6%uFF0C%u6765%u642D%u914Dkarma%u4F7F%u7528%u3002%0A%0Akarma%u662F%u4E00%u4E2A%u81EA%u52A8%u5316%u6D4B%u8BD5%u6846%u67B6%u3002%u56E0%u5176%u4E30%u5BCC%u7684%u914D%u7F6E%u9009%u9879%uFF0C%u826F%u597D%u7684%u6269%u5C55%u6027%u80FD%uFF0C%u88AB%u5E7F%u6CDB%u5E94%u7528%uFF0C%u5C24%u5176%u662F%u5728%u524D%u7AEF%u3002%u56E0%u4E3A%u524D%u7AEF%u8981%u9488%u5BF9%u5404%u79CD%u7248%u672C%u7684%u6D4F%u89C8%u5668%uFF0C%u5404%u79CD%u6846%u67B6%uFF0C%u5982%u679C%u5168%u90E8%u9760%u624B%u52A8%u53BB%u9002%u914D%uFF0C%u57FA%u672C%u4E0D%u592A%u53EF%u80FD%u3002%u6240%u4EE5%u5927%u5BB6%u90FD%u7528karma%u4E86%u3002%0A%23%23%20karma%20setup%0A%u57FA%u672C%u914D%u7F6E%u53C2%u8003%5B%u524D%u7AEF%u5355%u5143%u6D4B%u8BD5%u4E4BKarma%u73AF%u5883%u642D%u5EFA%5D%28https%3A//segmentfault.com/a/1190000006895064%29%0A%0A%u914D%u7F6E%u6587%u4EF6%u7B80%u5355%u5982%u4E0B%uFF1A%0A%60%60%60javascript%0Amodule.exports%20%3D%20function%28config%29%20%7B%0A%20%20config.set%28%7B%0A%0A%20%20%20%20//%20base%20path%20that%20will%20be%20used%20to%20resolve%20all%20patterns%20%28eg.%20files%2C%20exclude%29%0A%20%20%20%20basePath%3A%20%27%27%2C%0A%0A%0A%20%20%20%20//%20frameworks%20to%20use%0A%20%20%20%20//%20available%20frameworks%3A%20https%3A//npmjs.org/browse/keyword/karma-adapter%0A%20%20%20%20frameworks%3A%20%5B...%5D%2C%0A%0A%0A%20%20%20%20//%20list%20of%20files%20/%20patterns%20to%20load%20in%20the%20browser%0A%20%20%20%20files%3A%20%5B%0A%20%20%20%20%20%20%27...%27%2C%0A%20%20%20%20%20%20%7Bpattern%3A%20%27...%27%7D%0A%20%20%20%20%5D%2C%0A%0A%0A%20%20%20%20//%20list%20of%20files%20to%20exclude%0A%20%20%20%20exclude%3A%20%5B%0A%20%20%20%20%5D%2C%0A%0A%0A%20%20%20%20//%20preprocess%20matching%20files%20before%20serving%20them%20to%20the%20browser%0A%20%20%20%20//%20available%20preprocessors%3A%20https%3A//npmjs.org/browse/keyword/karma-preprocessor%0A%20%20%20%20preprocessors%3A%20%7B%0A%20%20%20%20%7D%2C%0A%0A%0A%20%20%20%20//%20test%20results%20reporter%20to%20use%0A%20%20%20%20//%20possible%20values%3A%20%27dots%27%2C%20%27progress%27%0A%20%20%20%20//%20available%20reporters%3A%20https%3A//npmjs.org/browse/keyword/karma-reporter%0A%20%20%20%20reporters%3A%20%5B%27progress%27%5D%2C%0A%0A%0A%20%20%20%20//%20web%20server%20port%0A%20%20%20%20port%3A%209876%2C%0A%0A%0A%20%20%20%20//%20enable%20/%20disable%20colors%20in%20the%20output%20%28reporters%20and%20logs%29%0A%20%20%20%20colors%3A%20true%2C%0A%0A%0A%20%20%20%20//%20level%20of%20logging%0A%20%20%20%20//%20possible%20values%3A%20config.LOG_DISABLE%20%7C%7C%20config.LOG_ERROR%20%7C%7C%20config.LOG_WARN%20%7C%7C%20config.LOG_INFO%20%7C%7C%20config.LOG_DEBUG%0A%20%20%20%20logLevel%3A%20config.LOG_INFO%2C%0A%0A%0A%20%20%20%20//%20enable%20/%20disable%20watching%20file%20and%20executing%20tests%20whenever%20any%20file%20changes%0A%20%20%20%20autoWatch%3A%20true%2C%0A%0A%0A%20%20%20%20//%20start%20these%20browsers%0A%20%20%20%20//%20available%20browser%20launchers%3A%20https%3A//npmjs.org/browse/keyword/karma-launcher%0A%20%20%20%20browsers%3A%20%5B%27...%27%5D%2C%0A%0A%0A%20%20%20%20//%20Continuous%20Integration%20mode%0A%20%20%20%20//%20if%20true%2C%20Karma%20captures%20browsers%2C%20runs%20the%20tests%20and%20exits%0A%20%20%20%20singleRun%3A%20false%2C%0A%0A%20%20%20%20//%20Concurrency%20level%0A%20%20%20%20//%20how%20many%20browser%20should%20be%20started%20simultaneous%0A%20%20%20%20concurrency%3A%20Infinity%2C%0A%0A%20%20%20%20//%20Which%20plugins%20to%20enable%0A%20%20%20%20plugins%3A%20%5B%5D%0A%20%20%7D%29%0A%7D%20%0A%60%60%60%0A%0A%u7B80%u5355%u7684%u89E3%u91CA%u76EE%u524D%u7528%u5230%u7684%u9009%u9879%uFF1A%0A-%20frameworks%3A%20%u4F7F%u7528%u5230%u7684%u6846%u67B6%uFF0C%u6240%u6709%u4F7F%u7528%u5230%u7684%uFF0C%u8981%u5728%u4E0B%u9762plugins%u4E2D%u5217%u660E%u3002%u6BD4%u5982%uFF1Aframeworks%u4E2D%u5305%u542Bmocha%uFF0C%u5219plugins%u4E0B%u5C31%u8981%u5305%u542Bkarma-mocha%0A-%20basePath%2C%20files%2C%20exclude%3A%20%u8FD9%u4E09%u4E2A%u6307%u660E%u4E86karma%u8981%u52A0%u8F7D%u7684JS%u6587%u4EF6%0A%09-%20basePath%uFF1A%u57FA%u7840%u76EE%u5F55%uFF0C%u4F1A%u76F4%u63A5%u63D2%u5165%u5230files%uFF0Cexclude%u6307%u5B9A%u7684%u8DEF%u5F84%u524D%u9762%0A%09-%20files%uFF1A%u52A0%u8F7D%u6587%u4EF6%uFF0C%u652F%u6301%u901A%u914D%u7B26%u548C%u6B63%u5219%u5F0F%uFF0C%u901A%u8FC7%u5927%u62EC%u53F7%u8FD8%u53EF%u4EE5%u6DFB%u52A0%u914D%u7F6E%u9009%u9879%u3002%u901A%u914D%u7B26%27%5C*%5C*%27%u8868%u793A%u6240%u6709%u76EE%u5F55%uFF0C%27%5C*%27%u8868%u793A%u6240%u6709%u6587%u4EF6%uFF0C%27%5C*%5C*/*%27%u8868%u793A%u5176%u4E0B%u6240%u6709%u76EE%u5F55%u6240%u6709%u6587%u4EF6%u3002%0A%09-%20exclude%3A%20%u53EF%u4EE5%u5254%u9664files%u4E2D%u4E0D%u60F3%u8981%u7684%u6587%u4EF6%0A-%20browsers%uFF1A%u4F7F%u7528%u7684%u6D4F%u89C8%u5668%uFF0C%u5982%u679C%u60F3%u8981headless%u6D4B%u8BD5%u5219%u53EF%u7528PhantomJS%0A%0A%23%23%23%20karma%20with%20ES6%0A%u56E0%u4E3APhantomJS%u4E0D%u652F%u6301ES6%u8BED%u6CD5%28%u7F51%u4E0A%u8BF4%uFF0CPhantomJS%u751A%u81F3%u5BF9ES5%u7684%u652F%u6301%u90FD%u4E0D%u662F%u5F88%u597D%29%uFF0C%u6240%u4EE5karma%u8981%u6D4B%u8BD5%u5305%u542BES6%u8BED%u6CD5%u7684%u4EE3%u7801%uFF0C%u8FD8%u9700%u8981%u4E00%u756A%u914D%u7F6E%u3002%u7F51%u4E0A%u76F8%u5173%u7684%u5E16%u5B50%u5F88%u591A%uFF0C%u5217%u4E3E%u5982%u4E0B%uFF1A%0A-%20%5BTesting%20ES6%20with%20Karma%2C%20RequireJS%2C%20and%20Angular%5D%28http%3A//radify.io/blog/announcing-karma-es6-shim/%29%20%20-%20karma-es6-shim%20%0A-%20%5BHow%20to%20Start%20Writing%20Your%20AngularJS%20Tests%20in%20ES6%5D%28https%3A//www.barbarianmeetscoding.com/blog/2015/11/16/how-to-start-writing-your-angular-js-tests-in-es6/%29%20-%20karma-babel-preprocessor%0A-%20%5BWriting%20Jasmine%20Unit%20Tests%20In%20ES6%5D%28http%3A//www.syntaxsuccess.com/viewarticle/writing-jasmine-unit-tests-in-es6%29%20-%20webpack%0A%0A%u603B%u7684%u6765%u8BF4%uFF0C%u6709%u8FD9%u4E48%u51E0%u79CD%u89E3%u51B3%u65B9%u6848%uFF1A%0A-%20karma-es6-shim%0A-%20karma-babel-preprocessor%0A-%20webpack%0A-%20SlimerJS%0A%u524D%u4E09%u79CD%u6709%u70B9%u590D%u6742%uFF0C%u76EE%u524D%u7684%u6211%u7684%u6280%u672F%u6808%u8FD8%u65E0%u6CD5%u7406%u89E3%u3002%u6700%u7EC8%u9009%u62E9%u6700%u7B80%u5355%u7684SlimerJS%u3002%u5728Linux%u4E0A%u53EF%u4EE5%u901A%u8FC7Xvfb%u505A%u5230headless%u6D4B%u8BD5%uFF0CWindows%u4E0A%u5C31%u8FD9%u6837%u5427%uFF0C%u8DF3%u4E2A%u7A97%u53E3%u4E5F%u4E0D%u662F%u4EC0%u4E48%u5927%u4E8B%u3002%0A%0A%23%23%23%20Debug%20karma%20test%20using%20Chrome%0A%u8FD9%u4E2A%u5728%u521D%u5B66%u7684%u65F6%u5019%u8FD8%u662F%u5F88%u91CD%u8981%u7684%uFF0C%u5982%u679C%u6CA1%u6709%u8FD9%u4E2A%u5DE5%u5177%uFF0C%u6050%u6015%u6211%u4E5F%u8DF3%u4E0D%u51FA%u81EA%u5DF1%u7ED9%u81EA%u5DF1%u6316%u7684%u5927%u5751%u3002%0A%u53C2%u8003%u94FE%u63A5%uFF1A%5BDebugging%20Karma%20Unit%20Tests%5D%28https%3A//glebbahmutov.com/blog/debugging-karma-unit-tests/index.html%29%0A%u6309%u7167%u4E0A%u9762%u7684%u6B65%u9AA4%u6253%u5F00%u8C03%u8BD5%u9875%u9762%u548CChrome%20Dev%20Tool%u9875%u9762%uFF0C%u5237%u65B0%u9875%u9762%u5373%u53EF%u91CD%u65B0%u52A0%u8F7D%u4EE3%u7801%u5E76%u89E6%u53D1%u65AD%u70B9%u3002%0A%u5C06%u547D%u4EE4%u884C%u5199%u5165package.json%uFF0C%u4EE5%u540E%u8C03%u7528npm%u5C31%u53EF%u4EE5%u65B9%u4FBF%u7684%u6253%u5F00%u8BE5%u8C03%u8BD5%u5DE5%u5177%u3002%0A%60%60%60%0A%7B%0A...%0A%20%20%22scripts%22%3A%20%7B%0A%20%20%20%20%22start%22%3A%20%22node%20./bin/www%22%2C%0A%20%20%20%20%22karma-debug%22%3A%20%22cd%20public/js%20%26%26%20karma%20start%20--browsers%3DChrome%20--single-run%3Dfalse%20--debug%22%0A%20%20%7D%2C%20%0A%20%20...%0A%7D%0A%60%60%60%0A%23%23%20ngMock%0AngMock%u4E2D%u4E24%u4E2A%u6700%u91CD%u8981%u7684%u5173%u952E%u8BCD%u5C31%u662F%uFF1Amodule%2C%20inject%u3002%u53EF%u4EE5%u53C2%u8003%u8FD9%u4E2A%u4EBA%u7684%u535A%u5BA2%uFF1A%0A-%20%5BngMock%20Fundamentals%20for%20AngularJS%20-%20Understanding%20Inject%5D%28http%3A//www.bradoncode.com/blog/2015/05/27/ngmock-fundamentals-angularjs-testing-inject/%29%0A-%20%5BngMock%20Fundamentals%20for%20AngularJS%20Unit%20Testing%20-%20Understanding%20Module%5D%28http%3A//www.bradoncode.com/blog/2015/05/24/ngmock-fundamentals-angularjs-unit-testing/%29%0A%0AngMock%u901A%u8FC7module%u52A0%u8F7D%u5DF2%u7ECF%u5B9A%u4E49%u4E86%u7684%u6A21%u5757%uFF0C%u6216%u8005%u5B9A%u4E49%u533F%u540Dmodule%u5E76%u63D0%u4F9B%u65B0%u7684service%uFF0C%u6216%u8005%u590D%u5199%u5DF2%u5B58%u5728%u7684service%u3002module%u53EA%u662F%u5C06%u540D%u5B57%uFF0C%u4EE5%u53CA%u5BF9%u5E94%u4F4D%u7F6E%u5B58%u5165%u4E00%u4E2A%u6570%u7EC4%u3002inject%u5C06%u771F%u6B63%u7684%u52A0%u8F7D%u6240%u6709module%u63D0%u4F9B%u7684service%uFF0C%u5E76%u89E3%u51B3%u4F9D%u8D56%u5173%u7CFB%uFF0C%u8FD8%u53EF%u4EE5%u5C06%u76F8%u5E94%u7684service%u5BFC%u51FA%u5230test%20case%u4E2D%u4F7F%u7528%u3002%u5176%u5B9EngMock%u4E5F%u5C31%u662F%u505A%u4E86%u8FD9%u4E24%u4EF6%u4E8B%u60C5%u3002%0A%u4E0B%u9762%u7B80%u5355%u4ECB%u7ECD%u4E00%u4E9B%u6709%u52A9%u4E8E%u7406%u89E3%u7684%u77E5%u8BC6%u70B9%u3002%0A%23%23%23%20angular.mock.module%0Amodule%u51FD%u6570%u7684%u539F%u578B%u662F%60angular.mock.module%28alias%2C%20obj%2C%20func%29%60%0Amodule%u63A5%u53D7%u4E09%u79CD%u53C2%u6570%u7C7B%u578B%uFF0C%u6BCF%u4E00%u79CD%u90FD%u53EF%u4EE5%u5355%u72EC%u4F7F%u7528%uFF0C%u7528%u6765%u5B9A%u4E49%u6216%u52A0%u8F7D%u5BF9%u5E94%u6A21%u5757%0A-%20alias%3A%20%u662Fmodule%u540D%u5B57%u5B57%u7B26%u4E32%uFF0C%u53EF%u4EE5%u662F%u5728%u5F85%u6D4B%u4EE3%u7801%u4E2D%u5B9A%u4E49%u7684%uFF0C%u4E5F%u53EF%u4EE5%u662F%u5728%u6D4B%u8BD5%u6587%u4EF6%u4E2D%u5B9A%u4E49%u7684%0A-%20obj%3A%20%u4E00%u4E2A%u666E%u901A%u7684%u5B57%u5178%u7ED3%u6784%uFF0C%u4ED6%u4F1A%u751F%u6210%u4E00%u4E2A%u533F%u540Dmodule%uFF0C%u5176%u4E2D%u7684%u6210%u5458%u4F1A%u88AB%u5B9A%u4E49%u6210%60%24provide.value%28%29%60%0A-%20function%3A%20%u540C%u6837%u662F%u5B9A%u4E49%u4E00%u4E2A%u533F%u540Dmodule%uFF0C%u4E0D%u540C%u7684%u662F%uFF0Cfunction%u53EF%u4EE5%u7528%24provide%u4F5C%u4E3A%u53C2%u6570%uFF0C%u6765%u5B9A%u4E49%u5404%u79CD%u5404%u6837%u7684service%u3002%0A%0A%3E**function%28%29%20vs%20Object**%0A%0A%3EThe%20examples%20we%20used%20were%20similar%2C%20so%20what%u2019s%20the%20key%20difference%20between%20the%20function%28%29%20and%20Object%20form%3F%20As%20the%20Object%20type%20gets%20added%20as%20a%20%24provider.value%2C%20it%20is%20therefore%20restricted%20to%20the%20capabilities%20of%20the%20value%20service%2C%20most%20importantly%20that%20a%20value%20service%20cannot%20be%20injected%20with%20other%20services.%0A%0A%u5173%u4E8E%24provide%uFF0C%u53EF%u4EE5%u53C2%u8003%5BAngular%u5B98%u65B9%u6587%u6863%5D%28https%3A//docs.angularjs.org/api/auto/service/%5C%24provide%29%0A%3EThe%20**%5C%24provide**%20service%20has%20a%20number%20of%20methods%20for%20registering%20components%20with%20the%20**%5C%24injector**.%20Many%20of%20these%20functions%20are%20also%20exposed%20on%20angular.Module.%0A%0A%u5B83%u63D0%u4F9B%u4E86%u4E0B%u9762%u7684%u8FD9%u4E9Bservice%0A%0A%3E**provider**%28name%2C%20provider%29%20-%20registers%20a%20service%20provider%20with%20the%20%5C%24injector%0A**constant**%28name%2C%20obj%29%20-%20registers%20a%20value/object%20that%20can%20be%20accessed%20by%20providers%20and%20services.%0A**value**%28name%2C%20obj%29%20-%20registers%20a%20value/object%20that%20can%20only%20be%20accessed%20by%20services%2C%20not%20providers.%0A**factory**%28name%2C%20fn%29%20-%20registers%20a%20service%20factory%20function%20that%20will%20be%20wrapped%20in%20a%20service%20provider%20object%2C%20whose%20%5C%24get%20property%20will%20contain%20the%20given%20factory%20function.%0A**service**%28name%2C%20Fn%29%20-%20registers%20a%20constructor%20function%20that%20will%20be%20wrapped%20in%20a%20service%20provider%20object%2C%20whose%20%5C%24get%20property%20will%20instantiate%20a%20new%20object%20using%20the%20given%20constructor%20function.%0A**decorator**%28name%2C%20decorFn%29%20-%20registers%20a%20decorator%20function%20that%20will%20be%20able%20to%20modify%20or%20replace%20the%20implementation%20of%20another%20service.%0A%0A%23%23%23%20angular.mock.inject%0A%5C%24inject%u662FngMock%u7684%u6838%u5FC3%uFF0C%u770B%u770B%u5B98%u65B9%u7684%u5B9A%u4E49%0A%3EThe%20inject%20function%20wraps%20a%20function%20into%20an%20injectable%20function.%20The%20inject%28%29%20creates%20new%20instance%20of%20%24injector%20per%20test%2C%20which%20is%20then%20used%20for%20resolving%20references.%0A%0Ainject%u5C31%u662F%u5B9A%u4E49%u4E86%u4E00%u4E2Ainjector%uFF0C%u90A3injector%u53C8%u662F%u4EC0%u4E48%0A%3E%5C%24injector%20is%20used%20to%20retrieve%20object%20instances%20as%20defined%20by%20provider%2C%20instantiate%20types%2C%20invoke%20methods%2C%20and%20load%20modules.%0A%0A%5C%24injector%u4E5F%u5C31%u662FAngularJS%u7684%u6838%u5FC3%u3002%u5B83%u63D0%u4F9B%u4E86%u6240%u8C13%u7684%u201CDI%22%20%28Dependency%20Injection%29%u6A21%u5F0F%u3002Angular%u7684%u6838%u5FC3%u4EE3%u7801%u901A%u8FC7%5C%24injector%u6765%u83B7%u53D6%u5BF9%u5E94service%u7684reference%u3002%0A%u6211%u4EEC%u901A%u5E38%u5C06inject%u653E%u5728beforeEach%u91CC%uFF0C%u8FD9%u6837inject%u5C31%u4E3A%u6BCF%u4E2A%u6D4B%u8BD5%u5B9A%u4E49%u4E00%u4E2Ainjector%uFF0C%u4E3A%u8FD9%u4E2Ainjector%u51C6%u5907%u597D%u6240%u6709%u7684service%uFF0C%u5E76%u80FD%u901A%u8FC7%u53C2%u6570%u5C06%u4EFB%u610F%u5DF2%u6709%u7684service%u5BFC%u51FA%u3002%0A%0A%23%23%23%20Test%20controllers%0A%u4E86%u89E3%u4E86%u4E0A%u9762%u7684%u4E24%u4E2A%u91CD%u8981%u7EC4%u4EF6%u4E4B%u540E%uFF0C%u9488%u5BF9%u4E0D%u540C%u7684%u6D4B%u8BD5%u7EC4%u4EF6%u5C31%u5BB9%u6613%u7406%u89E3%u4E86%u3002%0A%u76F4%u63A5%u4E0A%u4EE3%u7801%uFF1A%0A%60%60%60javascript%0Adescribe%28%27main%20controller%27%2C%20function%28%29%20%7B%0A%20%20var%20%24controller%0A%20%20var%20mainCtrl%0A%20%20beforeEach%28%27set%20up%20module%27%2C%20function%28%29%20%7B%0A%20%20%20%20module%28function%20%28%24provide%29%20%7B%0A%20%20%20%20%20%20%24provide.service%28%27MockDataService%27%2C%20function%20%28%29%20%7B%0A%20%20%20%20%20%20%20%20const%20service%20%3D%20this%0A%20%20%20%20%20%20%20%20service.data%20%3D%20%27test%20data%27%0A%20%20%20%20%20%20%7D%29%0A%20%20%20%20%7D%29%0A%0A%20%20%20%20module%28%27MainModule%27%29%0A%20%20%7D%29%3B%0A%0A%20%20beforeEach%28%27set%20up%20controller%27%2C%20inject%28function%20%28_%24controller_%2C%20MockDataService%29%20%7B%0A%20%20%20%20%24controller%20%3D%20_%24controller_%0A%20%20%20%20mainCtrl%20%3D%20%24controller%28%27MainController%27%2C%20%7B%0A%20%20%20%20%20%20%20%20%20%20DataService%3A%20MockDataService%0A%20%20%20%20%7D%29%0A%20%20%7D%29%29%20%0A%0A%20%20it%28%27should%20assemble%20print%20page%20URL%20correctly%27%2C%20function%28%29%20%7B%0A%20%20%20%20mainCtrl.getData%28%29.should.equal%28%27test%20data%27%29%0A%20%20%7D%29%3B%0A%7D%29%3B%0A%60%60%60%0A%u8FD9%u91CC%u53EA%u6709%u4E24%u4E2A%u503C%u5F97%u6CE8%u610F%u7684%u5730%u65B9%uFF0C%u5176%u5B9E%u524D%u6587%u4E5F%u90FD%u6709%u63D0%u53CA%u3002%0A1.%20%u7B2C%u4E00%u4E2Amodule%u4E2D%u901A%u8FC7%5C%24provide%u63D0%u4F9B%u4E86%u4E00%u4E2A%u5047%u7684service%uFF0C%u8FD9%u4E2Aservice%u53EF%u4EE5%u901A%u8FC7%u5176%u540D%u5B57%u88ABinject%u3002%0A2.%20inject%u4E2D%u901A%u8FC7%5C_%5C%24controller%5C_%u6765%u5BFC%u51FAcontroller%u7684%u6784%u9020%u51FD%u6570%u3002%u901A%u8FC7%u8FD9%u4E2A%u6784%u9020%u51FD%u6570%u6765%u6784%u9020%u76F8%u5E94%u7684%u5F85%u6D4Bcontroller%uFF0C%u5E76%u4F20%u5165%u5047%u7684service%u8FBE%u5230mock%u7684%u76EE%u7684%u3002%0A%0A%23%23%23%20Test%20services%0A%u4ECD%u7136%u76F4%u63A5%u4E0A%u4EE3%u7801%uFF1A%0A%60%60%60%20javascript%0Aangular.module%28%27PrinterData%27%2C%20%5B%5D%29%0A.config%28%5B%27%24locationProvider%27%2C%20function%28%24locationProvider%29%20%7B%0A%20%20%24locationProvider.html5Mode%28true%29%3B%0A%7D%5D%29%3B%0A%60%60%60%0A%60%60%60javascript%0Adescribe%28%27printer%20data%20service%27%2C%20function%28%29%20%7B%0A%20%20var%20%24httpBackend%0A%20%20var%20PrinterDataService%0A%20%20beforeEach%28function%28%29%7B%0A%20%20%20%20module%28function%20%28%24provide%29%20%7B%0A%20%20%20%20%20%20%24provide.provider%28%27%24location%27%2C%20function%20%28%29%20%7B%0A%20%20%20%20%20%20%20%20const%20provider%20%3D%20this%0A%20%20%20%20%20%20%20%20const%20location%20%3D%20%7B%7D%0A%20%20%20%20%20%20%20%20location.search%20%3D%20function%20%28%29%7B%0A%20%20%20%20%20%20%20%20%20%20return%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20id%3A%20%27test%20id%27%2C%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20provider.html5Mode%20%3D%20sinon.stub%28%29%0A%20%20%20%20%20%20%20%20provider.%24get%20%3D%20sinon.stub%28%29.returns%28location%29%0A%20%20%20%20%20%20%7D%29%0A%20%20%20%20%7D%29%0A%20%20%7D%29%3B%0A%20%20beforeEach%28function%28%29%20%7B%0A%20%20%20%20module%28%27Data%27%29%0A%20%20%20%20inject%28function%20%28%24injector%29%20%7B%0A%20%20%20%20%20%20%24httpBackend%20%3D%20%24injector.get%28%27%24httpBackend%27%29%0A%20%20%20%20%20%20DataService%20%3D%20%24injector.get%28%27DataService%27%29%0A%20%20%20%20%7D%29%0A%20%20%7D%29%3B%0A%0A%20%20it%28%27should%20update%20if%20succeeds%27%2C%20function%28done%29%20%7B%0A%20%20%20%20%24httpBackend.whenGET%28/.*/%29%0A%20%20%20%20%20%20.respond%28%7B%0A%20%20%20%20%20%20%20%20status%3A%20%27ready%27%0A%20%20%20%20%20%20%7D%29%0A%20%20%20%20DataService.getStatus%28%29.then%28function%20%28data%29%20%7B%0A%20%20%20%20%20%20data.should.deepEqual%28%7B%0A%20%20%20%20%20%20%20%20status%3A%20%27ready%27%0A%20%20%20%20%20%20%7D%29%0A%20%20%20%20%20%20done%28%29%0A%20%20%20%20%7D%29.catch%28function%20%28err%29%20%7B%0A%20%20%20%20%20%20should.not.exist%28err%29%0A%20%20%20%20%7D%29%0A%20%20%20%20%0A%20%20%20%20%24httpBackend.flush%28%29%3B%0A%20%20%7D%29%3B%0A%7D%29%0A%60%60%60%0A%u4E0B%u9762%u7F57%u5217%u4E00%u4E9B%u5B9E%u9645%u4F7F%u7528%u4E2D%u78B0%u5230%u7684%u4E00%u4E9B%u95EE%u9898%uFF0C%u4EE5%u53CA%u81EA%u5DF1%u7684%u4E00%u4E9B%u7406%u89E3%u3002%0A%0A%23%23%23%23%20%u4F7F%u7528%5C%24injector%0A%u6211%u4EEC%u8BF4%u8FC7inject%u51FD%u6570%u5C31%u662F%u7528%u6765%u521B%u5EFA%5C%24injector%uFF0C%u6240%u4EE5%5C%24injector%u4E5F%u662F%u53EF%u4EE5%u76F4%u63A5%u88AB%u5BFC%u51FA%u4F7F%u7528%u7684%u3002%u8FD9%u65F6%u9700%u8981%u4EC0%u4E48service%uFF0C%u5C31%u53EF%u4EE5%u76F4%u63A5%u8C03%u7528%5C%24injector.get%u3002%u4E3A%u4EC0%u4E48%u4E0D%u76F4%u63A5%u901A%u8FC7%u53C2%u6570%u6765inject%u5462%uFF1F%u5F53%u7136%u662F%u56E0%u4E3A%u53C2%u6570%u592A%u957F%u4E86%uFF0C%u4E0D%u597D%u770B%u3002%u3002%u3002%0A%23%23%23%23%20mock%20%5C%24http%0A%u4E3A%u4E86%u6D4B%u8BD5%u7A33%u5B9A%u6027%uFF0C%u6211%u4EEC%u5728unit%20test%u4E2D%u5E76%u4E0D%u771F%u6B63%u7684%u8BBF%u95EE%u670D%u52A1%u5668%uFF0C%u800C%u901A%u5E38%u91C7%u7528%u5047%u7684%u670D%u52A1%u5668%u54CD%u5E94%u6765mock%u3002%u8FD9%u5C31%u9700%u8981%u7528%u5230%5C%24httpBackend%20service%u3002%u90A3%u4E48%5C%24http%u548C%5C%24httpBackend%u4E4B%u95F4%u7684%u5173%u7CFB%u662F%u4EC0%u4E48%uFF1F%0A%3EHTTP%20backend%20used%20by%20the%20service%20that%20delegates%20to%20XMLHttpRequest%20object%20or%20JSONP%20and%20deals%20with%20browser%20incompatibilities.%0AYou%20should%20never%20need%20to%20use%20this%20service%20directly%2C%20instead%20use%20the%20higher-level%20abstractions%3A%20%5C%24http%20or%20%5C%24resource.%0ADuring%20testing%20this%20implementation%20is%20swapped%20with%20mock%20%24httpBackend%20which%20can%20be%20trained%20with%20responses.%0A%0A%u5177%u4F53%u505A%u6CD5%uFF0C%u4E0A%u9762%u7684%u4EE3%u7801%u4E2D%u5DF2%u6709%uFF0C%u8282%u5F55%u5982%u4E0B%uFF1A%0A%60%60%60javascript%0AbeforeEach%28function%28%29%20%7B%0A%20%20%20%20module%28%27Data%27%29%0A%20%20%20%20inject%28function%20%28%24injector%29%20%7B%0A%20%20%20%20%20%20%24httpBackend%20%3D%20%24injector.get%28%27%24httpBackend%27%29%0A%20%20%20%20%20%20DataService%20%3D%20%24injector.get%28%27DataService%27%29%0A%20%20%20%20%7D%29%0A%20%20%7D%29%3B%0A%0A%20%20it%28%27should%20update%20if%20succeeds%27%2C%20function%28done%29%20%7B%0A%20%20%20%20%24httpBackend.whenGET%28/.*/%29%0A%20%20%20%20%20%20.respond%28%7B%0A%20%20%20%20%20%20%20%20status%3A%20%27ready%27%0A%20%20%20%20%20%20%7D%29%0A%20%20%20%20DataService.getStatus%28%29.then%28function%20%28data%29%20%7B%0A%20%20%20%20%20%20data.should.deepEqual%28%7B%0A%20%20%20%20%20%20%20%20status%3A%20%27ready%27%0A%20%20%20%20%20%20%7D%29%0A%20%20%20%20%20%20done%28%29%0A%20%20%20%20%7D%29.catch%28function%20%28err%29%20%7B%0A%20%20%20%20%20%20should.not.exist%28err%29%0A%20%20%20%20%7D%29%0A%20%20%20%20%0A%20%20%20%20%24httpBackend.flush%28%29%3B%0A%20%20%7D%29%3B%0A%60%60%60%0A%u901A%u8FC7%60%24httpBackend.wheGET%28...%29.respond%28...%29%60%u6765%u8BBE%u7F6Emock%20response%u3002%60whenGET%60%u8FD8%u652F%u6301%u6B63%u5219%u5F0F%u3002%0A%u8C03%u7528%u5B8C%u8BF7%u6C42%u51FD%u6570%u540E%u8981%u8BB0%u5F97%60%24httpBackend.flush%28%29%60%u5C06%u8BF7%u6C42%u771F%u6B63%u7684%u53D1%u51FA%u3002%u5426%u5219%u8BF7%u6C42%u5E76%u4E0D%u4F1A%u53D1%u51FA%uFF0C%u76F4%u5230%u8C03%u7528%60flush%28%29%60%u51FD%u6570%uFF0C%u6216%u8005%u6D4B%u8BD5%u8D85%u65F6%u5931%u8D25%u3002%0A%0A%23%23%23%23%20%u5982%u4F55mock%20%5C%24location%20%26%20%5C%24locationProvider%0A%u5F88%u60ED%u6127%u7684%u8BF4%uFF0C%u5728%u8FD9%u4E2A%u95EE%u9898%u4E0A%u6211%u82B1%u4E86%u5F88%u4E45%u5F88%u4E45%u7684%u65F6%u95F4%uFF0C%u641C%u7D22%u4E86%u5927%u91CF%u7684google%u7F51%u9875%uFF0C%u6700%u540E%u8FD8%u662F%u4F9D%u9760chrome%u8FFD%u8E2Aangular%u6838%u5FC3%u4EE3%u7801%u624D%u53D1%u73B0%u539F%u6765%u662F%u81EA%u5DF1%u7ED9%u81EA%u5DF1%u6316%u7684%u5751%uFF0C%u5BF9service%2C%20factory%u548Cprovider%u7406%u89E3%u4E0D%u900F%u5F7B%u3002%0A%u9996%u5148%u6765%u5C31%u4E8B%u8BBA%u4E8B%uFF0C%u8BF4%u4E00%u4E0B%u5230%u5E95%u600E%u4E48%u5B8C%u6210%u6211%u7684%u8FD9%u4E2A%u6D4B%u8BD5%u3002%0A%0A%u56E0%u4E3A%u6211%u8981%u6D4B%u8BD5DataService%uFF0C%u800CDataService%u5B9A%u4E49%u5728Data%20moduleDataService%u8C03%u7528%u4E86%5C%24location%20service%u3002%u9700%u8981%u5C06%u4E4Bmock%u6389%u3002%u6700%u7B80%u5355%u7684%u60F3%u6CD5%u5C31%u662F%u901A%u8FC7inject%u6765%u83B7%u5F97%5C%24location%20service%20reference%u3002%u5E76%u901A%u8FC7sinon.stub%u6765%u8BBE%u7F6E%u9700%u8981%u7684%u8FD4%u56DE%u503C%u3002%0A%0A%60%60%60%0Ainject%28function%20%28%24injector%2C%20%24location%29%20%7B%0A%20%20sinon.stub%28%24location%2C%20%27search%27%29.returns%28%7B%0A%20%20%20%20id%3A%20%27test%20id%27%0A%20%20%7D%29%0A%20%20DataService%20%3D%20%24injector.get%28%27DataService%27%29%0A%7D%29%0A%60%60%60%0A%0A%u4F46%u662F%u6211%u5012%u9709%u50AC%u7684%uFF0C%u7ADF%u7136%u662F%u7528%5C%24provide%u505A%u4E86%uFF1A%0A%60%60%60%0Amodule%28function%20%28%24provide%29%20%7B%0A%20%20%24provide.service%28%27%24location%27%2C%20function%20%28%29%20%7B%0A%20%20%20%20const%20location%20%3D%20this%0A%20%20%20%20location.search%20%3D%20sinon.stub%28%29.returns%28%7B%0A%20%20%20%20%20%20device_id%3A%20%27test%20id%27%2C%0A%20%20%20%20%7D%29%0A%20%20%7D%29%0A%7D%29%0A%60%60%60%0A%u8FD9%u6837%u5C31%u4F1A%u6709%u95EE%u9898%u4E86%uFF0C%u5F53ngMock%u52A0%u8F7D%u8FD9%u4E2A%u6A21%u5757%u7684%u65F6%u5019%uFF0C%u4F1A%u5148%u5B9A%u4E49%u4E00%u4E2A%5C%24locationProvider%uFF0C%u8FD9%u5C31%u4F1A%u8986%u76D6%u5176%u81EA%u5E26%u7684%5C%24locationProvider%u3002%u800C%u5982%u4E0B%u7684module.config%u65B9%u6CD5%u4E2D%u5BF9%5C%24locationProvider%u7684%u64CD%u4F5C%u5C31%u4F1A%u51FA%u73B0%u95EE%u9898%uFF1A%0A%60%60%60%0Aangular.module%28%27PrinterData%27%2C%20%5B%5D%29%0A.config%28%5B%27%24locationProvider%27%2C%20function%28%24locationProvider%29%20%7B%0A%20%20%24locationProvider.html5Mode%28true%29%3B%0A%7D%5D%29%3B%0A%60%60%60%0A%u56E0%u4E3A%u65B0%u5B9A%u4E49%u7684%5C%24locationProvider%u4E2D%u6CA1%u6709html5Mode%u6210%u5458%u3002%u4E8E%u662F%u60F3%u4E86%u5404%u79CD%u529E%u6CD5%u6765mock%20%5C%24locationProvider%u3002%u7ED3%u679C%u56E0%u4E3A%u5BF9service%uFF0Cfactory%uFF0Cprovider%u7684%u7406%u89E3%u4E0D%u6B63%u786E%uFF0C%u90FD%u5931%u8D25%u4E86%u3002%u653E%u4E00%u6BB5%u9519%u8BEF%u7684%u4EE3%u7801%uFF1A%0A%60%60%60%0AbeforeEach%28function%28%29%7B%0A%20%20module%28function%20%28%24provide%29%20%7B%0A%20%20%20%20%24provide.service%28%27%24locationProvider%27%2C%20function%20%28%29%20%7B%0A%20%20%20%20%20%20const%20location%20%3D%20this%0A%20%20%20%20%20%20location.html5Mode%20%3D%20function%28%29%7B%7D%0A%20%20%20%20%7D%29%0A%20%20%20%20%24provide.factory%28%27%24location%27%2C%20function%20%28%29%20%7B%0A%20%20%20%20%20%20const%20locationProvider%20%3D%20this%0A%20%20%20%20%20%20locationProvider.html5Mode%20%3D%20function%28%29%7B%7D%0A%20%20%20%20%7D%29%0A%7D%29%0A%60%60%60%0A%u7B2C%u4E00%u4E2A%u7528%5C%24provide.service%u4F1A%u5B9A%u4E49%u4E00%u4E2A%5C%24locationProviderProvider%0A%u7B2C%u4E8C%u4E2A%u7528%5C%24provide.factory%u5B9A%u4E49%u4E86%u4E00%u4E2A%5C%24locationProvider%uFF0C%u4F46%u662F%u6CA1%u6709%u8FD4%u56DE%uFF0C%u6240%u4EE5%5C%24location%20service%u4E0D%u4F1A%u88AB%u5B9E%u4F8B%u5316%uFF0C%u4E5F%u5C31%u6CA1%u6709%5C%24location%20service%u4E86%u3002%0A%u5982%u679C%u60F3%u901A%u8FC7mock%20%5C%24locationProvider%u6765mock%20%5C%24location%20service%u7684%u8BDD%uFF0C%u6B63%u786E%u7684%u505A%u6CD5%u5E94%u5F53%u662F%uFF1A%0A%60%60%60%0A%24provide.provider%28%27%24location%27%2C%20function%20%28%29%20%7B%0A%20%20const%20provider%20%3D%20this%0A%20%20const%20location%20%3D%20%7B%7D%0A%20%20location.search%20%3D%20function%20%28%29%7B%0A%20%20%20%20return%20%7B%0A%20%20%20%20%20%20%20device_id%3A%20%27test%20id%27%0A%20%20%20%20%7D%0A%20%20%7D%0A%20%20provider.html5Mode%20%3D%20sinon.stub%28%29%0A%20%20provider.%24get%20%3D%20sinon.stub%28%29.returns%28location%29%0A%7D%29%0A%60%60%60%0Amock%u6389provider.%5C%24get%u51FD%u6570%uFF0C%u63D0%u4F9B%u5047%u7684location%u670D%u52A1%u3002%u5176%u5B9E%u8FD8%u6709%u5F88%u591A%u529E%u6CD5%uFF0C%u4F8B%u5982%u83B7%u5F97%5C%24locationProvider%u518D%u5BF9%5C%24get%u51FD%u6570%u8FDB%u884Cstub%uFF0C%u4E5F%u53EF%u4EE5%u3002%u6CA1%u6709%u518D%u4E00%u4E00%u5C1D%u8BD5%u4E86%u3002%u7406%u89E3%u4E86%u9519%u8BEF%u548C%u6B63%u786E%u7684%u505A%u6CD5%uFF0C%u60F3%u518D%u53BB%u5B9E%u73B0%u5C31%u5F97%u5FC3%u5E94%u624B%u4E86%u3002%0A%0A%23%23%23%23%20%u6D4B%u8BD5.config/.run%u5757%0AData%20module%u6709%u4E00%u4E2A.config%u5757%uFF0C%u56E0%u4E3A%u6A21%u5757%u52A0%u8F7D%u7684%u65F6%u5019%u4F1A%u5148%u8FD0%u884C%u5176.config%u5757%uFF0C%u7136%u540E%u662F.run%u5757%u3002%u56E0%u5176%u7279%u6B8A%u6027%uFF0C%u6240%u4EE5%u5728%u5BF9%u5176%u4E2D%u4F7F%u7528%u5230%u7684%u670D%u52A1%u8FDB%u884Cmock%u8981%u6CE8%u610F%u987A%u5E8F%u3002%u53EF%u4EE5%u53C2%u8003%u8FD9%u4E2A%u94FE%u63A5%uFF1A%5BTesting%20config%20and%20run%20blocks%20in%20AngularJS%0AHow%20to%20easily%20test%20an%20often%20neglected%20part%20of%20your%20application%5D%28https%3A//medium.com/@a_eife/testing-config-and-run-blocks-in-angularjs-1809bd52977e%29%0A%0A%23%23%23%23%20%u5982%u4F55mock%20%24interval%0A%u5728%u5355%u5143%u6D4B%u8BD5%u4E2D%uFF0C%u6211%u4EEC%u901A%u5E38%u8981%u91C7%u7528mock%u7684%u624B%u6BB5%uFF0C%u800C%u4E0D%u771F%u6B63%u7684%u4F7F%u7528%u65F6%u95F4%u670D%u52A1%u3002%u5728sinon%u4E2D%uFF0C%u53EF%u4EE5%u4F7F%u7528fake%20timer%u6765%u53D6%u4EE3JS%u4E2D%u6807%u51C6%u7684setTimeout%u548CsetInterval%u3002%u4E0D%u8FC7%u8FD9%u4E2A%u5728Angular%20JS%u4E2D%u5E76%u4E0D%u8D77%u4F5C%u7528%uFF0C%u5982%u679C%u6211%u4EEC%u4F7F%u7528%5C%24interval%u670D%u52A1%u7684%u8BDD%u3002%0A%u56E0%u4E3A%u5728%u5BF9Angular%20JS%u505A%u5355%u5143%u6D4B%u8BD5%u7684%u65F6%u5019%uFF0C%5C%24interval%u5E76%u4E0D%u4F1A%u8C03%u7528setInterval%u3002%u539F%u56E0%u662F%u8FD9%u91CC%u7684%5C%24interval%u5176%u5B9E%u662FngMock%u4E2D%u7684%5C%24interval%u3002%u6B64%u65F6%u53EF%u4EE5%u4F7F%u7528%60%24interval.flush%28ms%29%60%u6765%u5C06%u65F6%u949F%u63A8%u8FDB%u82E5%u5E72%u65F6%u95F4%u3002%u5F53%u7136%u5982%u679C%u613F%u610F%uFF0C%u4E5F%u53EF%u4EE5%u7528sinon%u6765mock%u4ECE%5C%24injector%u4E2Dget%u51FA%u6765%u7684%5C%24interval%20service%u3002%5C%24timeout%20service%u4E5F%u7C7B%u4F3C%uFF0C%u6709%u540C%u6837%u7684%u65B9%u6CD5%u5BF9%u4ED8%u5355%u5143%u6D4B%u8BD5%u3002%0A%0A%23%23%23%23%20Promise%20not%20resolve%0A%u5728Angular%20JS%u4E2D%uFF0C%u6211%u4EEC%u5F80%u5F80%u8C03%u7528%5C%24q%20service%u6765%u4F7F%u7528Promise%uFF0C%u800C%5C%24q.defer%28%29.promise%u53EA%u5728%5C%24scope%u7684digest%20cycle%u624D%u4F1A%u5224%u5B9Aresolve%u8FD8%u662Freject%uFF0C%u6240%u4EE5%u8981%u89E6%u53D1%5C%24q%u4EA7%u751F%u7684promise%uFF0C%u4E00%u5B9A%u8981%u8C03%u7528%u5BF9%u5E94%u7684%5C%24scope.%5C%24apply%28%29%u6216%u8005%5C%24scope.%5C%24digest%28%29%u3002stack%20overflow%u4E2D%u4E5F%u6709%u5927%u628A%u7C7B%u4F3C%u95EE%u9898%u7684%u89E3%u7B54%u3002%u5177%u4F53%u4E5F%u53EF%u4EE5%u53C2%u8003%u8FD9%u7BC7%u535A%u5BA2%uFF0C%u8BB2%u5F97%u5F88%u900F%u5F7B%uFF1A%0A-%20%5BUnit%20Testing%20with%20%24q%20Promises%20in%20AngularJS%5D%28http%3A//www.bradoncode.com/blog/2015/07/13/unit-test-promises-angualrjs-q/%29%0A%0A%u5728Angular%20JS%u4E2D%uFF0C%u4E5F%u53EF%u4EE5%u4F7F%u7528%u6807%u51C6%u7684Promise%uFF0C%u4F8B%u5982%3A%20%60var%20promise%20%3D%20new%20Promise%28resolve%2C%20reject%29%60%u3002%u4F46%u662F%u8C8C%u4F3C%uFF0C%u6807%u51C6%u7684Promise%u548C%5Cq%u517C%u5BB9%u7684%u5E76%u4E0D%u5F88%u597D%uFF0C%u4E0B%u9762%u7684code%u5E76%u4E0D%u4F1A%u5DE5%u4F5C%uFF1A%0A%60%60%60%0Avar%20promise%20%3D%20%24q.all%28%5Bpromise1%2C%20promise2%5D%29%0A%24scope.%24apply%28%29%0A%60%60%60%0Apromise%u5E76%u4E0D%u4F1A%u5728promise1%u548Cpromise2%u8FD4%u56DE%u4E4B%u540E%u8FD4%u56DE%uFF0C%u5B9E%u9645%u4E0A%u662F%u4ED6%u6839%u672C%u5C31%u4E0D%u4F1A%u8FD4%u56DE%uFF0C%u76F4%u5230%u6D4B%u8BD5%u8D85%u65F6%u5931%u8D25%u3002%0A%0A%23%23%23%23%20%22Unexpected%20request%3A%20GET%20.../.html%22%20on%20Karma%20tests%0A%u8FD9%u662FYaakov%u5728%u89C6%u9891%u4E2D%u63D0%u5230%u7684%u9700%u8981%u4F7F%u7528%5C%24templateCache%u7684%u60C5%u5F62%u3002Yaakov%u63D0%u5230%u7528karma%u662F%u89E3%u51B3%u8BE5%u95EE%u9898%u6700%u597D%u7684%u529E%u6CD5%uFF0C%u4F46%u662F%u56E0%u4E3A%u89C6%u9891%u5E76%u4E0D%u6D89%u53CA%u4ECB%u7ECDkarma%uFF0C%u6240%u4EE5%u5E76%u6CA1%u6709%u8BF4%u5E94%u8BE5%u600E%u4E48%u505A%u3002%0A%u5F53%u6211%u4EEC%u7684karma%20test%u52A0%u8F7D%u4E86route.js%uFF0C%u5E76%u5728%u5176%u4E2D%u5B9A%u4E49%u4E86component%uFF0C%u5219%u6D4B%u8BD5%u5728%u5B9E%u4F8B%u5316%u8BE5Angular%20component%u7684%u65F6%u5019%u56DE%u53BB%u7528%5C%24http%20service%u6765GET%u8BE5template%20html%u6587%u4EF6%uFF0C%u4E8E%u662F%u4EA7%u751F%u4E86%u8BE5unexpected%20request%u9519%u8BEF%u3002Yaakov%u5728%u6559%u7A0B%u4E2D%u7684%u89E3%u51B3%u529E%u6CD5%u662F%uFF0C%u901A%u8FC7%u4E00%u6B21%u771F%u6B63%u7684AJAX%u4ECE%u670D%u52A1%u5668%u4E0A%u8BF7%u6C42%u8BE5html%uFF0C%u5E76%u5C06%u4E4B%u5B58%u5165%5C%24templateCache%u4E2D%uFF0C%u4E4B%u540E%u5C31%u4E0D%u4F1A%u518D%u6709%u771F%u6B63%u7684GET%20AJAX%u4EA7%u751F%u4E86%u3002%u5176%u5B9E%u8FD9%u4E00%u6BB5%u53EF%u4EE5%u901A%u8FC7%u914D%u7F6Ekarma%u6765%u5B9E%u73B0%u3002%u5177%u4F53%u505A%u6CD5%u5982%u4E0B%uFF1A%0A-%20%u5B89%u88C5ng-html2js%uFF0Ckarma-ng-html2js-preprocessor%0A-%20%u4F7F%u7528ng-html2js%u5C06%u5BF9%u5E94%u7684template%20html%u8F6C%u6210js%u6A21%u5757%uFF0C%u5E76%u5728%u6D4B%u8BD5%u4EE3%u7801%u4E2D%u5F15%u5165%u3002%0A%0A%u770B%u4E0B%u9762%u5177%u4F53%u4EE3%u7801%0Akarma.conf.js%0A%60%60%60%0A//%20list%20of%20files%20/%20patterns%20to%20load%20in%20the%20browser%0Afiles%3A%20%5B%0A%20%20%27../html/templates/*.html%27%2C%0A%20%20...%0A%5D%2C%0A%0A//%20preprocess%20matching%20files%20before%20serving%20them%20to%20the%20browser%0A//%20available%20preprocessors%3A%20https%3A//npmjs.org/browse/keyword/karma-preprocessor%0Apreprocessors%3A%20%7B%0A%20%20%27../html/templates/home.template.html%27%3A%20%5B%27ng-html2js%27%5D%0A%7D%2C%0A%0AngHtml2JsPreprocessor%3A%20%7B%0A%20%20moduleName%3A%20%27templates%27%2C%0A%20%20stripPrefix%3A%20%27.*/%27%2C%0A%20%20prependPrefix%3A%20%27/html/templates/%27%0A%7D%2C%20%0A%60%60%60%0Atest.js%0A%60%60%60%0Adescribe%28function%28%29%7B%0A%20%20beforeEach%28module%28%27templates%27%29%29%0A%7D%29%0A%60%60%60%0A%u7279%u522B%u8981%u6CE8%u610F%u7684%u662F%u5BF9ng-html2js%u7684%u914D%u7F6E%0A%60%60%60%0AngHtml2JsPreprocessor%3A%20%7B%0A%20%20moduleName%3A%20%27templates%27%2C%0A%20%20stripPrefix%3A%20%27.*/%27%2C%0A%20%20prependPrefix%3A%20%27/html/templates/%27%0A%7D%2C%20%0A%60%60%60%0A%u5F53%u6CA1%u6709%60stripPrefix%60%u9009%u9879%u65F6%uFF0C%u901A%u8FC7Chrome%20debug%u53EF%u4EE5%u770B%u5230%uFF0C%u6240%u6709%u7684template%u90FD%u88AB%u8F6C%u5316%u6210%u4E86js%u6A21%u5757%uFF0C%u4F46%u662F%u5176%u6A21%u5757%u540D%u662F%u7C7B%u4F3C%60c%3A%5Cpath%5Cto%5Ctemplate%5Ctemplate.html%60%u7684%u7ED3%u6784%uFF0C%u800C%u6211%u5728route%u4E2D%u7684%u6307%u5B9A%u8DEF%u5F84%u662Fweb%u8DEF%u5F84%60templateUrl%3A%20%27/html/templates/home.template.html%27%60%u3002%u5C31%u5BF9%u4E0D%u4E0A%u53F7%u4E86%u3002%u505A%u6CD5%u662F%u901A%u8FC7%60stripPrefix%60%u5C06%u6240%u6709%u7684%u524D%u7F00%u5168%u90E8%u5265%u53BB%uFF0C%u7136%u540E%u7528%60prependPrefix%60%u52A0%u4E0A%u9700%u8981%u7684%u524D%u7F00%u3002%u8FD9%u6837%u5C31%u53EF%u4EE5%u5DE5%u4F5C%u4E86%u3002%u7F51%u4E0A%u6709%u5F88%u591A%u5E16%u5B50%u8BA8%u8BBA%u8FD9%u4E2A%u95EE%u9898%u3002%u5217%u4E24%u4E2A%u6709%u542F%u53D1%u6027%u7684%uFF1A%0A-%20%5BUnit%20Testing%20AngularJS%20directive%20with%20templateUrl%5D%28https%3A//stackoverflow.com/questions/15214760/unit-testing-angularjs-directive-with-templateurl%29%0A-%20%5BKarma%20%27Unexpected%20Request%27%20when%20testing%20angular%20directive%2C%20even%20with%20ng-html2js%5D%28https%3A//stackoverflow.com/questions/22869668/karma-unexpected-request-when-testing-angular-directive-even-with-ng-html2js%29%0A