在JavaScript的AngularJS库中进行单元测试的方法


Posted in Javascript onJune 23, 2015

开发者们都一致认为单元测试在开发项目中十分有好处。它们帮助你保证代码的质量,从而确保更稳定的研发,即使需要重构时也更有信心。

在JavaScript的AngularJS库中进行单元测试的方法

 测试驱动开发流程图

AngularJS的代码声称其较高的可测性确实是合理的。单单文档中列出端对端的测试实例就能说明。就像AngularJS这样的项目虽然都说单元测试很简单但真正做好却不容易。即使官方文档中以提供了详尽的实例,但在我的实际应用中却还是很有挑战。这里我就简单示范一下我是怎么操作的吧.

Instant Karma

Karma 是来Angular团队针对JavaScript开发的一个测试运行框架。它很方便的实现了自动执行测试任务从而替代了繁琐的手工操作(好比回归测试集或是加载目标测试的依赖关系)Karma 和Angular的协作就好比花生酱和果冻.

只需要在Karma中定义好配置文件启动它,接下来它就会在预期的测试环境下的自动执行测试用例。你可以在配置文件中制定相关的测试环境。angular-seed,是我强烈推荐的可以快速实施的方案。在我近期的项目中Karma 的配置如下:
 

module.exports = function(config) {
  config.set({
    basePath: '../',
 
    files: [
      'app/lib/angular/angular.js',
      'app/lib/angular/angular-*.js',
      'app/js/**/*.js',
      'test/lib/recaptcha/recaptcha_ajax.js',
      'test/lib/angular/angular-mocks.js',
      'test/unit/**/*.js'
    ],
 
    exclude: [
      'app/lib/angular/angular-loader.js',
      'app/lib/angular/*.min.js',
      'app/lib/angular/angular-scenario.js'
    ],
 
    autoWatch: true,
 
    frameworks: ['jasmine'],
 
    browsers: ['PhantomJS'],
 
    plugins: [
      'karma-junit-reporter',
      'karma-chrome-launcher',
      'karma-firefox-launcher',
      'karma-jasmine',
      'karma-phantomjs-launcher'
    ],
 
    junitReporter: {
      outputFile: 'test_out/unit.xml',
      suite: 'unit'
    }
 
  })
}

这个跟angular-seed的默认配置类似只不过有以下几点不同:

  •     需要更改浏览器从Chrome 转到PhantomJS, 这样每次跳转时无需再打开新的浏览器窗口,但在OSX系统会有窗口延迟。所以这个插件还有浏览器设置都做了更改。
  •     由于我的应用需要引用Google的Recaptcha服务因此添加了依赖的recaptcha_ajax.js小文件。这个小配置就像在Karma的配置文件中添加一行代码那么简单。

autoWatch确实是个很酷的设置,它会让Karma在有文件更改时自动回归你的测试用例。你可以这样安装Karma:
 

npm install karma

angular-seed 提供了一个简单的脚本inscripts/test.sh去触发Karma的测试。

用Jasmine设计测试用例

当使用Jasmine----一种行为驱动开发模式的JavaScript测试框架为Angular设计单元测试用例时大部分的资源都已可获取。

这也就是我接下来要说的话题。

如果你要对AngularJS controller做单元测试可以利用Angular的依赖注入dependency injection 功能导入测试场景中controller需要的服务版本还能同时检查预期的结果是否正确。例如,我定义了这个controller去高亮需要导航去的那个页签:
 

app.controller('NavCtrl', function($scope, $location) {
  $scope.isActive = function(route) {
    return route === $location.path();
  };
})

如果想要测试isActive方法,我会怎么做呢?我将检查$locationservice 变量是否返回了预期值,方法返回的是否预期值。因此在我们的测试说明中我们会定义好局部变量保存测试过程中需要的controlled版本并在需要时注入到对应的controller当中。然后在实际的测试用例中我们会加入断言来验证实际的结果是否正确。整个过程如下:
 

describe('NavCtrl', function() {
  var $scope, $location, $rootScope, createController;
 
  beforeEach(inject(function($injector) {
    $location = $injector.get('$location');
    $rootScope = $injector.get('$rootScope');
    $scope = $rootScope.$new();
 
    var $controller = $injector.get('$controller');
 
    createController = function() {
      return $controller('NavCtrl', {
        '$scope': $scope
      });
    };
  }));
 
  it('should have a method to check if the path is active', function() {
    var controller = createController();
    $location.path('/about');
    expect($location.path()).toBe('/about');
    expect($scope.isActive('/about')).toBe(true);
    expect($scope.isActive('/contact')).toBe(false);
  });
});

使用整个基本的结构,你就能设计各种类型的测试。由于我们的测试场景使用了本地的环境来调用controller,你也可以多加上一些属性接着执行一个方法清除这些属性,然后再验证一下属性到底有没有被清除。

$httpBackendIs Cool

那么要是你在调用$httpservice请求或是发送数据到服务端呢?还好,Angular提供了一种

$httpBackend的mock方法。这样的话,你就能自定义服务端的响应内容,又或是确保服务端的响应结果能和单元测试中的预期保持一致。

具体细节如下:
 

describe('MainCtrl', function() {
  var $scope, $rootScope, $httpBackend, $timeout, createController;
  beforeEach(inject(function($injector) {
    $timeout = $injector.get('$timeout');
    $httpBackend = $injector.get('$httpBackend');
    $rootScope = $injector.get('$rootScope');
    $scope = $rootScope.$new();
 
 
    var $controller = $injector.get('$controller');
 
    createController = function() {
      return $controller('MainCtrl', {
        '$scope': $scope
      });
    };
  }));
 
  afterEach(function() {
    $httpBackend.verifyNoOutstandingExpectation();
    $httpBackend.verifyNoOutstandingRequest();
  });
 
  it('should run the Test to get the link data from the go backend', function() {
    var controller = createController();
    $scope.urlToScrape = 'success.com';
 
    $httpBackend.expect('GET', '/slurp?urlToScrape=http:%2F%2Fsuccess.com')
      .respond({
        "success": true,
        "links": ["http://www.google.com", "http://angularjs.org", "http://amazon.com"]
      });
 
    // have to use $apply to trigger the $digest which will
    // take care of the HTTP request
    $scope.$apply(function() {
      $scope.runTest();
    });
 
    expect($scope.parseOriginalUrlStatus).toEqual('calling');
 
    $httpBackend.flush();
 
    expect($scope.retrievedUrls).toEqual(["http://www.google.com", "http://angularjs.org", "http://amazon.com"]);
    expect($scope.parseOriginalUrlStatus).toEqual('waiting');
    expect($scope.doneScrapingOriginalUrl).toEqual(true);
  });
});

正如你所见,beforeEach call其实都很类似,唯一不同的是我们是从injector获取$httpBackend而并非直接获取。即使如此,创建不同的测试时还会有一些明显的不同之处。对初学者来说,会有一个afterEachcall 方法来确保$httpBackend在每次用例执行后不会有明显的异常请求。如果你观察一下测试场景的设置和$httpBackend方法的应用就会会发现有那么几点不是那么直观的。
 

实际上调用$httpBackend的方法也算是简单明了但还不够——我们还得在传值给$scope.$apply的方法中把调用封装到实际测试中的$scope.runTest方法上。这样在$digest被触发后才能处理HTTP请求。而如你所见直到我们调用$httpBackend.flush()方法后$httpBackend才会被解析,这也就保证了我们能在调用过程中去验证返回的结果是否正确(在上面的示例中,controller的$scope.parseOriginalUrlStatusproperty属性将被传递给调用者,我们也因此能实时监控)

接下来的几行代码都是在调用过程中检测$scopethat属性的断言。很酷吧?

提示:在某些单元测试中,用户习惯把没有$的范围标记为变量。这个在Angular文档中并没有强制要求或是过分强调,只是我在使用中为了提高可读性和一致性才使用$scopelike这种方式。

结论

也许这就是我做起来对其他人而言只是自然而然能做到的事情之一,但是学习使用Angular编写单元测试一开始对我而言确实是相当痛苦的。我发现自己对如何开始的理解大多来自互联网上各种博客文章和资源的拼拼凑凑,没有真正一致或明确的最佳实践,而是通过自然而然随意的选择。我想针对我最终得到的成果提供一些文档,以帮助那些也许还在坑里面挣扎的其他人,毕竟他们只是想要编写代码而已,而非不得不去了解Angular和Jasmine中所有的怪异特性和独特用法。因此我希望这篇文章能对你有些许帮助。

Javascript 相关文章推荐
用JavaScript隐藏控件的方法
Sep 21 Javascript
一个通过script自定义属性传递配置参数的方法
Sep 15 Javascript
javascript复制粘贴与clipboardData的使用
Oct 16 Javascript
jQuery中DOM操作实例分析
Jan 23 Javascript
JavaScript数据结构链表知识详解
Nov 21 Javascript
浅谈jQuery this和$(this)的区别及获取$(this)子元素对象的方法
Nov 29 Javascript
巧用数组制作图片切换js代码
Nov 29 Javascript
js动态添加表格逐行添加、删除、遍历取值的实例代码
Jan 25 Javascript
了解ESlint和其相关操作小结
May 21 Javascript
jQuery pager.js 插件动态分页功能实例分析
Aug 02 jQuery
react native 仿微信聊天室实例代码
Sep 17 Javascript
js对象简介与基本用法示例
Mar 13 Javascript
javascript框架设计之框架分类及主要功能
Jun 23 #Javascript
js的flv视频播放器插件使用方法
Jun 23 #Javascript
使用Raygun来自动追踪AngularJS中的异常
Jun 23 #Javascript
使用JavaScript的AngularJS库编写hello world的方法
Jun 23 #Javascript
浅谈setTimeout 与 setInterval
Jun 23 #Javascript
简介可以自动完成UI的AngularJS工具angular-smarty
Jun 23 #Javascript
javascript中传统事件与现代事件
Jun 23 #Javascript
You might like
php中的观察者模式
2010/03/24 PHP
ThinkPHP模板Switch标签用法示例
2014/06/30 PHP
Javascript实例教程(19) 使用HoTMetal(1)
2006/12/23 Javascript
Extjs学习笔记之六 面版
2010/01/08 Javascript
jQuery ajax serialize()方法的使用以及常见问题解决
2013/01/27 Javascript
JS 删除字符串最后一个字符的实现代码
2014/02/20 Javascript
正则表达式基本语法及表单验证操作详解【基于JS】
2017/04/07 Javascript
jQuery读取本地的json文件(实例讲解)
2017/10/31 jQuery
vue-cli 打包后提交到线上出现 "Uncaught SyntaxError:Unexpected token" 报错
2018/11/06 Javascript
vue使用axios实现excel文件下载的功能
2020/07/16 Javascript
Vue单文件组件开发实现过程详解
2020/07/30 Javascript
在antd中setFieldsValue和defaultVal的用法
2020/10/29 Javascript
用pywin32实现windows模拟鼠标及键盘动作
2014/04/22 Python
在Python的setuptools框架下生成egg的教程
2015/04/13 Python
Python爬虫框架Scrapy实例代码
2018/03/04 Python
对Python 2.7 pandas 中的read_excel详解
2018/05/04 Python
Python常见数字运算操作实例小结
2019/03/22 Python
python获取指定日期范围内的每一天,每个月,每季度的方法
2019/08/08 Python
基于python cut和qcut的用法及区别详解
2019/11/22 Python
python模块hashlib(加密服务)知识点讲解
2019/11/25 Python
使用pytorch 筛选出一定范围的值
2020/06/28 Python
利用python爬取有道词典的方法
2020/12/08 Python
安纳塔拉酒店度假村及水疗官方网站:Anantara Hotel
2016/08/25 全球购物
商务会议邀请函
2014/01/09 职场文书
学校出纳员岗位职责
2014/03/18 职场文书
2014年综治宣传月活动总结
2014/04/28 职场文书
经济类毕业生求职信
2014/06/26 职场文书
庆元旦演讲稿
2014/09/15 职场文书
房产销售独家委托书范本
2014/10/01 职场文书
大学生党员自我剖析材料
2014/10/06 职场文书
质量负责人岗位职责
2015/02/15 职场文书
语文教师求职信范文
2015/03/20 职场文书
党员干部廉政承诺书
2015/04/28 职场文书
党支部综合考察意见
2015/06/01 职场文书
初三毕业感言
2015/07/31 职场文书
交互式可视化js库gojs使用介绍及技巧
2022/02/18 Javascript