这里有新鲜出炉的 AngularJS 教程,程序狗速度看过来!
AngularJS 诞生于 Google 是一款优秀的前端 JS 框架,已经被用于 Google 的多款产品当中。AngularJS 有着诸多特性,最为核心的是:MVC、模块化、自动化双向数据绑定、语义化标签、依赖注入,等等。
当 Angular 项目的规模到达一定的程度,就需要进行测试工作了。为什么要自动化测试?1,提高产出质量。2,减少重构时的痛。反正我最近重构多了,痛苦经历多了。3,便于新人接手。下面这篇文章就给大家详细介绍了 Angular.Js 的自动化测试,有需要的朋友们可以参考借鉴。
本文着重介绍关于 ng 的测试部分,主要包括以下三个方面:
下面各部分进行详细介绍。
测试的分类
在测试中,一般分为单元测试和端到端测试,单元测试是保证开发者验证代码某部分有效性的技术,端到端(E2E)是当你想确保一堆组件能按事先预想的方式运行起来的时候使用。
其中单元测试又分为两类: TDD(测试驱动开发) 和 BDD(行为驱动开发)。
下面着重介绍两种开发模式。
TDD(测试驱动开发 Test-driven development)是使用测试案例等来驱动你的软件开发。
如果我们想要更深入点了解 TDD,我们可以将它分成五个不同的阶段:
其中的流程图如下:
TDD
TDD 的好处:
BDD 是(行为驱动开发 Behavior-Driven Development)指的是不应该针对代码的实现细节写测试,而是要针对行为写测试。BDD 测试的是行为,即软件应该怎样运行。
最后总结:TDD 的迭代反复验证是敏捷开发的保障,但没有明确如何根据设计产生测试,并保障测试用例的质量,而 BDD 倡导大家都用简洁的自然语言描述系统行为的理念,恰好弥补了测试用例(即系统行为)的准确性。
测试框架选择
利用 karma 和 jasmine 来进行 ng 模块的单元测试。
Karma: 是一个基于 Node.js 的 JavaScript 测试执行过程管理工具,这个测试工具的一个强大特性就是,它可以监控 (Watch) 文件的变化,然后自行执行,通过 console.log 显示测试结果。
jasmine 是一个行为驱动开发(BDD)的测试框架, 不依赖任何 js 框架以及 dom, 是一个非常干净以及友好 API 的测试库.
Karma
karma 是一个单元测试的运行控制框架, 提供以不同环境来运行单元测试, 比如 chrome,firfox,phantomjs 等, 测试框架支持 jasmine,mocha,qunit, 是一个以 nodejs 为环境的 npm 模块.
Karma 从头开始构建,免去了设置测试的负担,集中精力在应用逻辑上。会产生一个浏览器实例,针对不同浏览器运行测试,同时可以对测试的运行进行一个实时反馈,提供一份 debug 报告。
测试还会依赖一些 Karma 插件,如测试覆盖率 Karma-coverage 工具、Karman-fixture 工具及 Karma-coffee 处理工具。此外,前端社区里提供里比较丰富的插件,常见的测试需求都能涵盖到。
安装测试相关的 npm 模块建议使用—-save-dev 参数, 因为这是开发相关的, 一般的运行 karma 的话只需要下面两个 npm 命令:
- npm install karma --save-dev
- npm install karma-junit-reporter --save-dev
然后一个典型的运行框架通常都需要一个配置文件, 在 karma 里可以是一个 karma.conf.js, 里面的代码是一个 nodejs 风格的, 一个普通的例子如下:
- module.exports = function(config){
- config.set({
- // 下面files里的基础目录
- basePath : '../',
- // 测试环境需要加载的JS信息
- files : [
- 'app/bower_components/angular/angular.js',
- 'app/bower_components/angular-route/angular-route.js',
- 'app/bower_components/angular-mocks/angular-mocks.js',
- 'app/js/**/*.js',
- 'test/unit/**/*.js'
- ],
- // 是否自动监听上面文件的改变自动运行测试
- autoWatch : true,
- // 应用的测试框架
- frameworks: ['jasmine'],
- // 用什么环境测试代码,这里是chrome`
- browsers : ['Chrome'],
- // 用到的插件,比如chrome浏览器与jasmine插件
- plugins : [
- 'karma-chrome-launcher',
- 'karma-firefox-launcher',
- 'karma-jasmine',
- 'karma-junit-reporter'
- ],
- // 测试内容的输出以及导出用的模块名
- reporters: ['progress', 'junit'],
- // 设置输出测试内容文件的信息
- junitReporter : {
- outputFile: 'test_out/unit.xml',
- suite: 'unit'
- }
- });
- };
运行时输入:
- karma start test/karma.conf.js
jasmine
jasmine 是一个行为驱动开发的测试框架, 不依赖任何 js 框架以及 dom, 是一个非常干净以及友好 API 的测试库.
以下以一个具体实例说明 test.js:
- describe("A spec (with setup and tear-down)",
- function() {
- var foo;
- beforeEach(function() {
- foo = 0;
- foo += 1;
- });
- afterEach(function() {
- foo = 0;
- });
- it("is just a function, so it can contain any code",
- function() {
- expect(foo).toEqual(1);
- });
- it("can have more than one expectation",
- function() {
- expect(foo).toEqual(1);
- expect(true).toEqual(true);
- });
- });
开始单元测试
下面分别以控制器,指令,过滤器和服务四个部分来编写相关的单元测试。项目地址为 angular-seed(点我) 项目,可以下载 demo 并运行其测试用例。
demo 中是一个简单的 todo 应用,会包含一个文本输入框,其中可以编写一些笔记,按下按钮可以将新的笔记加入笔记列表中,其中使用 notesfactory 封装 LocalStorage 来储存笔记信息。
先介绍一下 angular 中测试相关的组件 angular-mocks。
了解 angular-mocks
在 Angular 中,模块都是通过依赖注入来加载和实例化的,因此官方提供了 angular-mocks.js 测试工具来提供模块的定义、加载,依赖注入等功能。
其中一些常用的方法 (挂载在 window 命名空间下):
用来加载已有的模块,以及配置 inject 方法注入的模块信息。具体使用如下:
- angular.mock.module: module
- beforeEach(module('myApp.filters'));
- beforeEach(module(function($provide) {
- $provide.value('version', 'TEST_VER');
- }));
该方法一般在 beforeEach 中使用,在执行测试用例之前可以获得模块的配置。
用来注入配置好的 ng 模块,来供测试用例里进行调用。具体使用如下:
- angular.mock.inject: inject
- it('should provide a version', inject(function(mode, version) {
- expect(version).toEqual('v1.0.1');
- expect(mode).toEqual('app');
- }));
其实 inject 里面就是利用 angular.inject 方法创建的一个内置的依赖注入实例,然后里面的模块和普通的 ng 模块的依赖处理是一样的。
Controller 部分
Angular 模块是 todoApp,控制器是 TodoController,当按钮被点击时,TodoController 的
函数会被调用。下面是 app.js 的代码部分。
- createNote()
- var todoApp = angular.module('todoApp', []);
- todoApp.controller('TodoController',
- function($scope, notesFactory) {
- $scope.notes = notesFactory.get();
- $scope.createNote = function() {
- notesFactory.put($scope.note);
- $scope.note = '';
- $scope.notes = notesFactory.get();
- }
- });
- todoApp.factory('notesFactory',
- function() {
- return {
- put: function(note) {
- localStorage.setItem('todo' + (Object.keys(localStorage).length + 1), note);
- },
- get: function() {
- var notes = [];
- var keys = Object.keys(localStorage);
- for (var i = 0; i < keys.length; i++) {
- notes.push(localStorage.getItem(keys[i]));
- }
- return notes;
- }
- };
- });
在 todoController 中用了个叫做 notesFactory 的服务来存储和提取笔记。当 createNote() 被调用时,会使用这个服务将一条信息存入 LocalStorage 中,然后清空当前的 note。因此,在编写测试模块是,应该保证控制器初始化,scope 中有一定数量的笔记,在调用
之后,笔记的数量应该加一。
- createNote()
具体的单元测试如下:
- describe('TodoController Test',
- function() {
- beforeEach(module('todoApp')); // 将会在所有的it()之前运行
- // 我们在这里不需要真正的factory。因此我们使用一个假的factory。
- var mockService = {
- notes: ['note1', 'note2'],
- //仅仅初始化两个项目
- get: function() {
- return this.notes;
- },
- put: function(content) {
- this.notes.push(content);
- }
- };
- // 现在是真正的东西,测试spec
- it('should return notes array with two elements initially and then add one', inject(function($rootScope, $controller) { //注入依赖项目
- var scope = $rootScope.$new();
- // 在创建控制器的时候,我们也要注入依赖项目
- var ctrl = $controller('TodoController', {
- $scope: scope,
- notesFactory: mockService
- });
- // 初始化的技术应该是2
- expect(scope.notes.length).toBe(2);
- // 输入一个新项目
- scope.note = 'test3';
- // now run the function that adds a new note (the result of hitting the button in HTML)
- // 现在运行这个函数,它将会增加一个新的笔记项目
- scope.createNote();
- // 期待现在的笔记数目是3
- expect(scope.notes.length).toBe(3);
- }));
- });
在 beforeEach 中,每一个测试用例被执行之前,都需要加载模块
。
- module("todoApp")
由于不需要外部以来,因此我们本地建立一个假的 mockService 来代替 factory,用来模拟 noteFactory,其中包含相同的函数,
和
- get()
。这个假的 factory 从数组中加载数据代替 localStorage 的操作。
- put()
在 it 中,声明了依赖项目
和
- $rootScope
,都可以由 Angular 自动注入,其中
- $controller
用来获得根作用域,
- $rootScope
用作创建新的控制器。
- $controller
服务需要两个参数。第一个参数是将要创建的控制器的名称。第二个参数是一个代表控制器依赖项目的对象,
- $controller
方法将会返回一个新的作用域,它用来注入控制器。同时我们传入 mockService 作为假 factory。 之后,初始化会根据 notes 数组的长度预测笔记的数量,同时在执行了
- $rootScope.$new()
函数之后,会改变数组的长度,因此可以写出两个测试用例。
- createNote()
Factory 部分
factory 部分的单元测试代码如下:
- describe('notesFactory tests',
- function() {
- var factory;
- // 在所有it()函数之前运行
- beforeEach(function() {
- // 载入模块
- module('todoApp');
- // 注入你的factory服务
- inject(function(notesFactory) {
- factory = notesFactory;
- });
- var store = {
- todo1: 'test1',
- todo2: 'test2',
- todo3: 'test3'
- };
- spyOn(localStorage, 'getItem').andCallFake(function(key) {
- return store[key];
- });
- spyOn(localStorage, 'setItem').andCallFake(function(key, value) {
- return store[key] = value + '';
- });
- spyOn(localStorage, 'clear').andCallFake(function() {
- store = {};
- });
- spyOn(Object, 'keys').andCallFake(function(value) {
- var keys = [];
- for (var key in store) {
- keys.push(key);
- }
- return keys;
- });
- });
- // 检查是否有我们想要的函数
- it('should have a get function',
- function() {
- expect(angular.isFunction(factory.get)).toBe(true);
- expect(angular.isFunction(factory.put)).toBe(true);
- });
- // 检查是否返回3条记录
- it('should return three todo notes initially',
- function() {
- var result = factory.get();
- expect(result.length).toBe(3);
- });
- // 检查是否添加了一条新纪录
- it('should return four todo notes after adding one more',
- function() {
- factory.put('Angular is awesome');
- var result = factory.get();
- expect(result.length).toBe(4);
- });
- });
在 TodoController 模块中,实际上的 factory 会调用 localStorage 来存储和提取笔记的项目,但由于我们单元测试中,不需要依赖外部服务去获取和存储数据,因此我们要对
和
- localStorage.getItem()
进行 spy 操作,也就是利用假函数来代替这两个部分。
- localStorage.setItem()
是用来用假函数进行监听的。第一个参数指定需要监听的对象,第二个参数指定需要监听的函数,然后 andCallfake 这个 API 可以编写自己的函数。因此,测试中完成了对 localStorage 和 Object 的改写,使函数可以返回我们自己数组中的值。
- spyOn(localStorage,'setItem')andCallFake()
在测试用例中,首先检测新封装的 factory 函数是否包含了
和
- get()
这两个方法,,然后进行
- put()
操作后断言笔记的数量。
- factory.put()
Filter 部分
我们添加一个过滤器。truncate 的作用是如果传入字符串过长后截取前 10 位。源码如下:
- todoApp.filter('truncate',
- function() {
- return function(input, length) {
- return (input.length > length ? input.substring(0, length) : input);
- }
- });
所以在单元测试中,可以根据传入字符串的情况断言生成子串的长度。
- describe('filter test',function(){
- beforeEach(module('todoApp'));
- it('should truncate the input to 1o characters',inject(function(truncateFilter){
- expect(truncateFilter('abcdefghijkl',10).length).toBe(10);
- });
- );
- });
之前已经对断言进行讨论了,值得注意的一点是我们需要在调用过滤器的时候在名称后面加入 Filter,然后正常调用即可。
Directive 部分
源码中的指令部分:
- todoApp.directive('customColor',
- function() {
- return {
- restrict: 'A',
- link: function(scope, elem, attrs) {
- elem.css({
- 'background-color': attrs.customColor
- });
- }
- };
- });
由于指令必须编译之后才能生成相关的模板,因此我们要引入
服务来完成实际的编译,然后再测试我们想要进行测试的元素。
- $compile
会创建一个 jqLite 元素,然后我们将其编译到一个新生成的自作用域中,就可以被测试了。具体测试用例如下:
- angular.element()
- describe('directive tests',function(){
- beforeEach(module('todoApp'));
- it('should set background to rgb(128, 128, 128)',
- inject(function($compile,$rootScope) {
- scope = $rootScope.$new();
- // 获得一个元素
- elem = angular.element("<span custom-color=\"rgb(128, 128, 128)\">sample</span>");
- // 创建一个新的自作用域
- scope = $rootScope.$new();
- // 最后编译HTML
- $compile(elem)(scope);
- // 希望元素的背景色和我们所想的一样
- expect(elem.css("background-color")).toEqual('rgb(128, 128, 128)');
- })
- );
- });
开始端到端测试
在端到端测试中,我们需要从用户的角度出发,来进行黑盒测试,因此会涉及到一些 DOM 操作。将一对组件组合起来然后检查是否如预想的结果一样。
在这个 demo 中,我们模拟用户输入信息并按下按钮的过程,检测信息能否被添加到 localStorage 中。
在 E2E 测试中,需要引入 angular-scenario 这个文件,并且建立一个 html 作为运行 report 的展示,在 html 中包含带有 e2e 测试代码的执行 js 文件,在编写完测试之后,运行该 html 文件查看结果。具体的 e2e 代码如下:
- describe('my app',
- function() {
- beforeEach(function() {
- browser().navigateTo('../../app/notes.html');
- });
- var oldCount = -1;
- it("entering note and performing click",
- function() {
- element('ul').query(function($el, done) {
- oldCount = $el.children().length;
- done();
- });
- input('note').enter('test data');
- element('button').query(function($el, done) {
- $el.click();
- done();
- });
- });
- it('should add one more element now',
- function() {
- expect(repeater('ul li').count()).toBe(oldCount + 1);
- });
- });
我们在端到端测试过程中,首先导航到我们的主 html 页面 app/notes.html,可以通过
来完成,
- browser.navigateTo()
函数选择了 ul 元素并记录其中有多少个初始化的项目,存放在 oldCount 变量中。
- element.query()
然后通过
来键入一个新的笔记,然后模拟一下点击操作来检查是否增加了一个新的笔记(li 元素)。然后通过断言可以将新旧的笔记数进行对比。
- input('note').enter()
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。
来源: http://www.phperz.com/article/17/0707/329729.html