在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 相关文章推荐
node在两个div之间移动,用ztree实现
Mar 06 Javascript
提交表单时执行func方法实现代码
Mar 17 Javascript
JQuery自适应窗口大小导航菜单附源码下载
Sep 01 Javascript
jQuery设置图片等比例缩小的方法
Apr 29 jQuery
vue 监听屏幕高度的实例
Sep 05 Javascript
Vue中的作用域CSS和CSS模块的区别
Oct 09 Javascript
详解离线安装npm包的几种方法
Nov 25 Javascript
node中使用es6/7/8(支持性与性能)
Mar 28 Javascript
js module大战
Apr 19 Javascript
javascript实现抢购倒计时程序
Aug 26 Javascript
vue实现在v-html的html字符串中绑定事件
Oct 28 Javascript
jquery实现图片无缝滚动 蒙版遮蔽效果
Jan 11 jQuery
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实现批量查询清除一句话后门的代码
2008/01/20 PHP
php递归创建目录的方法
2015/02/02 PHP
使一个函数作为另外一个函数的参数来运行的javascript代码
2007/08/13 Javascript
JQuery 1.4 中的Ajax问题
2010/01/23 Javascript
jquery isEmptyObject判断是否为空对象的函数
2011/02/14 Javascript
了解一点js的Eval函数
2012/07/26 Javascript
jquery解决图片路径不存在执行替换路径
2013/02/06 Javascript
jQuery筛选器children()案例详解(图文)
2013/02/17 Javascript
如何使用jQUery获取选中radio对应的值(一句代码)
2013/06/03 Javascript
jquery实现初次打开有动画效果的网页TAB切换代码
2015/09/06 Javascript
jQuery siblings()用法实例详解
2016/04/26 Javascript
AngularJS 实现弹性盒子布局的方法
2016/08/30 Javascript
socket.io实现在线群聊功能
2017/04/07 Javascript
Vue.js学习教程之列表渲染详解
2017/05/17 Javascript
使用vue-cli+webpack搭建vue开发环境的方法
2017/12/22 Javascript
JS计算距当前时间的时间差实例
2017/12/29 Javascript
详解webpack4之splitchunksPlugin代码包分拆
2018/12/04 Javascript
vue实现多级菜单效果
2019/10/19 Javascript
Python实现将罗马数字转换成普通阿拉伯数字的方法
2017/04/19 Python
Python利用字典将两个通讯录文本合并为一个文本实例
2018/01/16 Python
20行Python代码实现视频字符化功能
2020/04/13 Python
python cookie反爬处理的实现
2020/11/01 Python
python爬虫请求头的使用
2020/12/01 Python
全网最细 Python 格式化输出用法讲解(推荐)
2021/01/18 Python
详解appium自动化测试工具(monitor、uiautomatorviewer)
2021/01/27 Python
康帕斯酒店预订:Compass Hospitality(支持中文)
2018/08/23 全球购物
FC-Moto美国:欧洲最大的摩托车服装和头盔商店之一
2019/08/24 全球购物
SQL Server里面什么样的视图才能创建索引
2015/04/17 面试题
what is the difference between ext2 and ext3
2013/11/03 面试题
大学生两会学习心得体会
2014/03/10 职场文书
工伤赔偿协议书
2014/04/15 职场文书
银行行长竞聘演讲稿
2014/04/23 职场文书
一帮一活动总结
2014/05/08 职场文书
谁动了我的奶酪读书笔记
2015/06/30 职场文书
详解分布式系统中如何用python实现Paxos
2021/05/18 Python
css如何把元素固定在容器底部的四种方式
2022/06/16 HTML / CSS