【AngularJS的概念及其单元测试】之基本概念
一、AngularJS基本概念
1)AngularJS框架的核心概念
AngularJS框架的核心概念是MVC架构模式(或者说MVVM,Model-View-ViewModel,这两个模式差别不大)。MVC架构模式可以讲整个应用划分成三个完全不相关的独立模块:
(1)模型Model:是整个应用的驱动力。一般来说,指的是应用从服务器端获取的数据,任何在UI上看到的数据都是从模型或模型的子集中获取的。
(2)视图View:是用户可以浏览并与之交互的UI界面。它是动态的,基于当前系统的模型。
(3)控制器Controller:代表业务逻辑和表现层。控制器负责具体实现方式来决定将哪些模型展现在视图中,因此它可以被看作是一个视图模型,或者展现器(presenter)。
这样将应用分拆成独立的子单元的好处是:
(1)每个单元只负责做一件事情,符合单一职责原则。模型层负责数据操作,视图层展现UI界面,控制器负责业务逻辑。
(2)每个单元之间相互独立,这使得模块化、可重用性和可维护性大大提高。
2)AngularJS的哲学
(1)数据驱动(通过数据绑定实现)
一种情形:
HTML:Hello <span id="name"></span>
Javascript:
var updateNameUI = function(name){
$('#name').text(name);
}
// 首次加载数据时
updateNameUI(user.name);
// 当数据变化时重新显示
updateNameUI(updatedName);
如上,显示数据需要找到对应的UI元素并更新它的innerText,每次名称变化,都不得不调用以此该函数。而AngularJS采用模型驱动应用,通过数据绑定来实现,既有单向绑定,也有双向绑定。
把数据绑定到HTML上,AngularJS会负责正确的将数据传递给UI,一旦数据变化,AngularJS会检查到变化并自动更新UI。单向绑定的实现方式:Hello <span ng-bind="name"></span>
或Hello <span>{ {name} }</span>
另一种情形:
<form name="myForm" onsubmit="submitData()">
<input type="text" id="nameField" />
<input type="text" id="emailField" />
</form>
function setUserDetails(userDetails){
$('#nameField').value(userDetails.name);
$('#emailField').value(userDetails.email);
}
function getUserDetails(){
return {
name: $('#nameFeild').value(),
email: $('#emailFeild').value()
}
}
var submitData = function(){
makeXhrRequest('http://url', getUserDetails);
}
上面这种情形,除了页面布局和模板之外,还需要编写代码来控制业务逻辑/控制器与UI之间的数据双向传递,需要自己手动更新数据和获取数据。AngularJS提供了双向数据绑定,不需要再编写额外的代码区传递数据。双向数据绑定可以让控制器和UI共享一个数据模型,任何一边修改了数据,都会导致另一边自动更新。实现方式:
<form name="myForm" onsubmit="submitData()">
<input type="text" ng-model="uer.name" />
<input type="text" ng-model="user.email" />
</form>
(2)声明
未经扩展的原生HTML模板很难体现出页面的构成,比如下面的结构:
<ul calss="nav nav-tabs">
<li>Home</li>
<li class="selected">Profile</li>
</ul>
<div class="tab1">Some Content Here</div>
<div class="tab2"><input id="startDate" type="text" /></div>
AngularJS定义了声明范式,直接在HTML里声明你想要什么就可以了。AngularJS通过声明实现上述功能,增强HTML,如下代码(IE8及以下版本不支持扩展HTML标签):
<tabs>
<tab title="Home">Some Content Here</tab>
<tab title="Profile"><input type="text" datepicker ng-model="startDate" /></tab>
</tabs>
这样的做法的好处是:
- 这是声明式的,看到HTML代码即可明白其结构,含有两个标签页,其中一个含有日历控件。
- 所有的功能都进行封装在指令中。
(3)概念分离
AngularJS的所有应用都可以归结于MVC架构:
- 不同部分之间的概念分离的非常清晰。业务逻辑只在控制器中定义,而渲染则限于视图。
- 其他成员很容易理解你的代码,因为代码结构和模式都是既定的。
- 需要进行修改时,只需要单独修改相关的部分,而不会影响其他模块。
- AngularJS又并不完全是MVC架构的,它的控制器并不直接持有视图的引用,这是一个进步,因为它让控制器能够独立于视图。在测试的时候就不需要再创建DOM节点了。
(4)依赖注入
依赖注入是指当我们需要某个具体的控制器或者服务时,并不用直接在代码中用new操作符或函数显示创建实例(类似DatabaseFactory.getInstance()),而是发送请求以获取它的所有依赖关系。
这样做的好处是我们并不需要关心如何构建这些依赖关系以及在开始之前就明确我们需要什么。
(5)可扩展性
如指令,极大地扩展了浏览器和HTML的功能。
3)angular应用中的四大金刚
控制器、服务、过滤器、指令
- 控制器:让数据与用户界面交互,也可以处理简单的页面风格和表现逻辑。
- 服务:用来创建常用的业务逻辑,它能共享于所有的控制器。
- 过滤器:用于处理数据,以及将数据格式化后呈现给用户。
- 指令:在指令中可以操作DOM,可以操作和渲染可重用的UI组件。
4)AngularJS的可测试性
AngularJS从控制器、服务、指令到视图、页面迁移都被设计成可测试性的。AngularJS中的控制器和视图是相互独立的,而且依赖注入的部分同样具有高可测试性。Karma提供了单元测试环境,Protractor是一个基于WebDriver的端到端测试环境。
二、单元测试的基本概念
1)对单元测试的理解
在百度百科上的解释:
-
单元unit:单元就是相对独立的功能模块。一个完整的、模块化的程序都是由许多单元构成,单元完成自己的任务、然后与其他单元进行交互,最终协同完成整个程序的功能。
-
测试test:测试就是判断测试对象对于某个特定的输入有没有预期的输出。
工程上的一个共识是:如果程序的每个模块都是正确的,模块与模块之间的连接是正确的,那么程序基本上就是正确的。
所以单元测试就是一种保证构成程序的每个模块的正确性,从而保证整个程序的正确性的方法论。单元测试(优先)的目的就是首先保证一个系统的基本组成单元、模块(如对象以及对象中的方法)能正常工作,这是一种分而治之中的bottom-up思想。
所以,为什么需要单元测试?
- 正确性:测试可以验证代码的正确性,在上线前做到心里有底。
- 自动化:当然手工也可以测试,通过console可以打印出内部信息,但是这是一次性的事情,下次测试还需要从头来过,效率不能得到保证。通过编写测试用例,可以做到一次编写,多次运行。
- 解释性:测试用例用于测试接口、模块的重要性,那么在测试用例中就会涉及如何使用这些API。其他开发人员如果要使用这些API,那阅读测试用例是一种很好地途径,有时比文档说明更清晰。
- 驱动开发,指导设计:代码被测试的前提是代码本身的可测试性,那么要保证代码的可测试性,就需要在开发中注意API的设计,TDD将测试前移就是起到这么一个作用。
- 保证重构:互联网行业产品迭代速度很快,迭代后必然存在代码重构的过程,那么要怎样才能保证重构后代码的质量呢?有测试用例做后盾,就可以大胆的进行重构,重构后只需运行一遍单元测试ut,就能保证代码中没有产生回归错误(regressions),而不是去改变ut。
2)单元测试的模式:TDD BDD
(1)TDD: Test-driven Development, 即测试驱动开发
基本思路是通过测试来推动整个开发的进行。原理是在开发功能代码之前,先编写单元测试用例代码,通过测试代码来确定需要编写什么产品代码。所以测试驱动开发不仅仅是将测试当成验证的工具,而是把需求分析、设计、质量控制量化的过程。
TDD的测试步骤是:先写测试——再写代码——测试——重构——通过
(2)BDD: Behavior-driven Development, 即行为驱动开发
BDD是TDD的进化,但关注的课核心是设计,在行为驱动开发中,定义系统的行为(由客户和开发者一起定义系统的行为,避免表达不一致带来的问题)是主要工作,而对系统的描述则成了测试标准。
一款BDD模式的测试框架:Jasmine
"Jasmine is a Behavior-Driven Development testing framework for JavaScript. It does not rely on browsers, DOM, or any JavaScript framework.Thus it's suited for websites, Node.js projects, or anywhere that JavaScript can run." —— 来自官网
3)单元测试举例
以最简单的运算类函数为例,一般能覆盖以下几种值就行了:
- 空值
- 普通合法值
- 边界合法值(最大/最小合法值)
- 边界非法值(最大/最小非法值)
- 普通非法值
- 极限值(最大/最小可能值)
- 特殊值(如果存在)
三、Test Runner 与 Test Framework
Karma是测试运行器,它只负责找出代码中所有的单元测试用例,然后打开浏览器并测试它们,最终获取测试结果。并不关心那些测试用例到底是用什么语言编写的,以及我们究竟采用的是什么框架,它所做的仅仅是运行这些测试而已。
Jasmine是一种测试框架,它定义了测试用例的语法和API,以及如何为这些用例编写断言。它还有其他的替代品,如mocha、qunit等。
四、Karma的基本概念(Test Runner)
Karma通过NodeJS和SocketIO来进行测试,适用于不同的浏览器,速度很快。
1)Karma插件的分类
(1)浏览器插件
如Chrome插件karma-chrome-launcher,也有对应的Firefox、IE等浏览器的插件。
(2)测试框架
可以选择采用哪种框架来编写但与测试,如Jasmine的插件karma-jasmine,也有其他风格的框架的插件,如mocha、qunit。
(3)报表生成
Karma的测试结果提供了丰富的格式,默认的报表生成器是内置的,报表插件如karma-html-reporter,karma-junit-reporter。
(4)集成
它们能够和已有的JS库或者工具进行整合,Karma插件涵盖了大部分主流的JS库。
2)Karma的安装与配置
(1)在项目根目录初始化 package.json
npm init
(2)安装 karma+jasmine 相关包
推荐为每个项目本地安装Karma,而不是安装一个全局的Karma。
npm install karma -g
npm install karma-jasmine -g
npm install karma-chrome-launcher -g
npm install karma-cli -g
npm install karma-coverage -g
npm install karma-html-reporter -g
npm install karma-junit-reporter -g
(jasmine-core)
(将上面的参数 -g 替换成 --save-dev 会在该项目内安装,而不是在全局安装,并且在 package.json 中 dev-dependencies 中显示依赖的相关包)
(2)配置
karma 提供了自动生成配置文件的方法。执行 karma init,按照提示回答几个问题即可,默认文件名 karma.config.js。
// 文件 karma.config.js
// Karma configuration
// Generated on Tue Nov 01 2016 14:17:00 GMT+0800 (中国标准时间)
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
// 放置文件的根目录
basePath: '',
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
// 使用哪些测试框架(jasmine/mocha/qunit/...),如Jasmine,需安装karma-jasmine插件
frameworks: ['jasmine'],
// list of files / patterns to load in the browser
// 浏览器需要加载的文件列表或者文件匹配表达式
// 需要加载入浏览器的js文件,包括基础的类库,被测试js源文件和测试用例js文件
// 如果需要测试angular代码,比如引入angular-mock.js,给angular代码进行mock
files: [
'../web/vender/jquery/jquery-1.10.2.min.js',
'../web/vender/angular/angular.min.js',
'../web/vender/angular/angular-ui-router.min.js',
'../web/vender/bootstrap_v3.3.5/js/bootstrap.min.js',
'lib/angular-mocks.js', // 注意angular-mock的版本一定要和angular版本一致
'../web/common/*.js',
/****/
'../web/bbs/template/*.html',
'../web/common/template/**/*.html',
/***/
'tc/ut/bbs/*.js'
],
// list of files to exclude 需要排除的文件列表或者文件匹配正则表达式
exclude: [
// 'tc/ut/apply/apply.js',
'tc/ut/project/my/task.js',
],
// test results reporter to use 这里定义输出的报告
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
// html对应karma-html-reporter组件,coverage对应karma-coverage组件,输出测试用例执行报告
reporters: ['progress', 'html', 'junit','coverage'],
junitReporter: {
// will be resolved to basePath (in the same way as files/exclude patterns)
outputFile: 'report/ut/test-results.xml',
suite: 'UT',
useBrowserName: false
},
htmlReporter: {
outputDir: 'report/ut',
reportName: 'result' // outputDir+reportName组成完整的输出报告格式,如没有定义,会自动生成浏览器+OS信息的文件夹,不方便读取报告
},
//定义需要统计覆盖率的文件
preprocessors: {
'../web/bbs/bbs.js':'coverage',
'../web/common/*.js':'coverage',
'../web/bbs/**/*.html': 'ng-html2js',
'../web/common/template/*.html': 'ng-html2js'
},
coverageReporter: {
type: 'cobertura', //'cobertura', //将覆盖率报告类型type设置为html
subdir:'coverage', //dir+subdir组成完整的输出报告格式,如没有定义,会自动生成浏览器+OS信息的文件夹,不方便读取报告
dir: 'report/ut/' //代码覆盖率报告生成地址
},
// 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
// 进行测试时是否允许随时监视文件变化(被测试文件和测试用用例文件),如有修改,自动重新执行测试
// 否则用户就得手动在终端中运行 karma run 进行另一轮测试,此命令可以让karma以当前的服务器配置再次进行相同的测试
autoWatch: true,
// Continuous Integration mode 持续集成模式(是否重复运行)
// if true, Karma captures browsers, runs the tests and exits
// 如果设置为true,则会启动浏览器,运行测试然后退出。在持续集成的环境下,该参数应该被设置为true
// 上一个参数为true,本参数为false,则自动监视才生效。否则执行完测试用例后自动退出
singleRun: false,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
// 用来执行自动监听的浏览器,推荐chrome
browsers: ['Chrome'],
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity,
/*captureTimeout : 60000,
browserDisconnectTimeout : 10000, // default 2000
browserDisconnectTolerance : 1, // default 0
browserNoActivityTimeout : 60000, //default 10000*/
ngHtml2JsPreprocessor: {
cacheIdFromPath: function(filepath) {
var cacheId = filepath.substr(filepath.lastIndexOf('/webapp/')+7);
// console.log(cacheId);
return cacheId;
},
moduleName: 'template'
}
});
};
(3)启动karma
运行 karma start,会在运行目录中搜索karma.conf.js文件并加载配置内容。如果配置文件的名称不是这个名字,或者在不同的目录中,可以讲完整路径作为参数传入:karma start [my.karma.conf.js]
(4)代码覆盖率
安装karma-coverage插件:
npm install karma-coverage --save-dev
修改karma.config.js, 增加覆盖率配置:
// 定义需要统计覆盖率的文件
preprocessors: {
'src/**/*.js': ['coverage']
},
reporters: ['progress', 'html', 'junit', 'coverage'], //在 reporters 中增加 coverage
coverageReporter: {
type: 'html', // 将覆盖率报告类型type设置为html // 'cobertura'
subdir:'coverage', // dir+subdir 组成完整的输出报告格式,如没有定义,会自动生成浏览器+OS信息的文件夹,不方便读取报告
dir: 'report/ut/' // 代码覆盖率报告生成地址
}
五、Jasmine的基本概念(Test Framework)
Jasmine框架是一种使用行为驱动的方式来构建测试的。具体说就是描述想要的行为并设定预期。
它的大部分的测试代码demo 参见 【AngularJS的概念及其单元测试】之Jasmine测试脚本Demo
六、参考
AngularJS:Up & Running (AngularJS即学即用)