AngularJs  unit-testing(单元测试)详解


Posted in Javascript onSeptember 02, 2016

 javascript是一门动态类型语言,这给她带来了很强的表现能力,但同时也使编译器几乎不能给开发者提供任何帮助。因为这个原因,我们感受到编写任何javascript代码都必须有一套强大完整的测试。angular拥有许多功能,让我们更加容易地测试我们的应用。我们应该没有借口不去写测试(这个嘛……)。

一、 It is all about NOT mixing concerns(全部都关于避免代码关系变得复杂……)

单元测试,正如名称那样,是关于测试单个“单元”的代码。单元测试努力解答这些问题:我对逻辑的考虑是否已经正确?排序方法得出的结果是否正确?为了解答这些问题,将这些问题独立出来显得尤其重要。这是因为当我们在测试排序方法的时候,我们不想关心其他相关的片段,例如DOM元素或者发起XHR请求获取数据等。明显地,通常比较难做到在典型的项目中单独调用一个函数。导致这个问题的原因是,开发者通常把关系弄得很复杂,最终让一个代码片段看起来可以做所有事情。它通过XHR获取数据,对数据进行排序,然后操纵DOM。与angular一起,我们可以更加容易地写出较好的代码,所以angular为我们提供XHR(我们可以模拟它)的依赖注入,angular还创建允许我们对model进行排序而不需要操作DOM的抽象。所以,到最后,我们可以简单地写一个排序方法,然后通过测试用例创建数据集合,供排序方法测试时使用,然后判断结果model是否符合预期。测试无须等待XHR、者创建对应的DOM和判断函数是否正确操作DOM。angular的核心思想包含代码的可测试性,但同时也要求我们去做正确的事情。angular致力于简化做正确事情的方法,但angular不是魔法,这意味着我们如果不遵循以下的几点,我们最终可能会得出一个不可测试的应用。

1. Dependency Inject

有许多办法可以获得依赖的资源:1)我们可以使用new操作符;2)我们使用一个众所周知的方式,被称为” 全局单例”;3)我们可以向registry service请求(但我们如何取得一个registry?可以查看后面的章节);4)我们可以期待它会被传递过来。

上面列出的方法中,只有最后一个是可测试的,让我们看看为什么:

1) Using the new operator

使用new操作符时基本上没有错误,但问题是通过new调用构造函数将会永久地将调用方与type绑定起来。举个例子,我们尝试实例化一个XHR对象,以让我们可以从服务器获得一些数据。

function MyClass() {
   this.doWork = function() {
     var xhr = new XRH();
     xhr.open(method,url,true);
     xhr.onreadystatechange = function() {…};
     xhr.send();
}
}

问题来了,在测试时,我们通常需要实例化一个可以返回测试数据或者网络错误的虚拟的XHR。通过调用new XHR(),我们永久地绑定了真实的XHR,并且没有一个很好的方法去替代它。当然,有一个糟糕的补救办法,有很多理由可以证明那是一个糟糕的想法:

var oldXHR = XHR;
XHR = new MockXHR() {};
myClass.doWork();
//判断MockXHR是否通过正常的参数进行调用
XHR = oldXHR;//如果忘了这一步,很容易会发生悲催的事情。

2) Global look-up

解决问题的另外一个方法是在一个众所周知的地方获取依赖的资源。

function MyClass() {
   this.doWork = function() {
      global.xhr({…});
  };
}

没有创建新依赖对象的实例的情况下,问题基本上与new一致,除了那个悲催的补丁以外,没有一个很好的方法可以再测试时拦截global.xhr的调用。测试的最基本的问题是global变量需要改为调用虚拟的方法而被修改。想进一步了解它的坏处,可以参观这里:http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/

上面的代码比较难去测试,所以我们必须修改global state:

var oldXHR = global.xhr;
global.xhr = function mockXHR(){…};
var myClass = new MyClass();
//判断MockXHR是否通过正常的参数进行调用
global.xhr = oldXHR;//如果忘了这一步,很容易会发生悲催的事情。

3) Service Registry

拥有一个包含所有service的registry的话,似乎可以解决问题,然后,在测试代码中替换所需要的service。

function MyClass() {
   var serviceRegistry = ???;
   this.doWork = function() {
     var xhr = serviceRegistry.get(“xhr”);
  …
  };
}

但是,serviceRegistry来自哪里?if it is: * new-ed up, the the test has no chance to reset the services for testing * global look-up, then the service returned is global as well (but resetting is easier, since there is only one global variable to be reset)(这里后面的文字跟乱码一样……没看懂)

根据这个方法,将上面的Class修改为如下的方式:

var oldServiceLocator = global.serviceLocator;
global.serviceLocator.set('xhr', function mockXHR() {});
var myClass = new MyClass();
myClass.doWork();
//判断MockXHR是否通过正常的参数进行调用
global.serviceLocator = oldServiceLocator; //如果忘了这一步,很容易会发生悲催的事情。

4) Passing in Dependencies

最后,依赖资源可以被传入。

function MyClass(xhr) {
   this.doWork = function() {
     xhr({…});
  };
}

这个是首选的方式,因为代码无须理会xhr是从哪来的,也不关心谁创建了传进来的xhr。因此,类的创建者与类的使用者可以分开编码,这将创建的责任从逻辑中分离出来,这就是依赖注入的概述。

这个class很容易测试,在测试中我们可以这样写:

function xhrMock(args) {…}
var myClass = new MyClass(xhrMock);
myClass.doWrok();
//做一些判断……
通过这个测试代码,我们可以意识到没有任何全局变量被破坏。

angular附带的dependency-injection(https://3water.com/article/91775.htm),通过这种方式编写的代码,更加容易编写测试代码,如果我们想编写可测试性强的代码,我们最好使用它。

2. Controllers

逻辑使每一个应用都是唯一的,这就是我们想去测试的。如果我们的逻辑里面混杂着DOM的操作,这将会跟下面的例子一样难测试:

function PasswordController() {
  // 获取DOM对象的引用
  var msg = $('.ex1 span');
  var input = $('.ex1 input');
  var strength;

  this.grade = function() {
     msg.removeClass(strength);
     var pwd = input.val();
     password.text(pwd);
     if (pwd.length > 8) {
        strength = 'strong';
     } else if (pwd.length > 3) {
        strength = 'medium';
     } else {
        strength = 'weak';
     }
    msg.addClass(strength).text(strength);
  }
}

上面的代码在测试时会遇到问题,因为它需要我们的执行测试时候,需要有正确的DOM。测试代码会如下:

var input = $('<input type="text"/>');
var span = $('<span>');
$('body').html('<div class="ex1">').find('div').append(input).append(span);
var pc = new PasswordController();
input.val('abc');
pc.grade();
expect(span.text()).toEqual('weak');
$('body').html('');

在angular中,controller严格地将DOM操作逻辑分离出来,将大大降低编写测试用例的难度,看看下面的例子:

function PasswordCntrl($scope) {
  $scope.password = '';
  $scope.grade = function() {
     var size = $scope.password.length;
     if (size > 8) {
          $scope.strength = 'strong';
     } else if (size > 3) {
          $scope.strength = 'medium';
     } else {
          $scope.strength = 'weak';
     }
  };
}

测试代码直截了当:

var pc = new PasswordController($scope);
pc.password('abc');
pc.grade();
expect($scope.strength).toEqual('weak');

值得注意的是,测试代码不仅仅更加间断,而且更加容易追踪。我们一直说测试用例是在讲故事,而不是判断其他不相关的东西。

3. Filters

filter(http://docs.angularjs.org/api/ng.$filter)是用于将数据转换为对用户友好的格式。它们很重要,因为它们将转换格式的责任从应用逻辑中分离出来,进一步简化了应用逻辑。

myModule.filter('length', function() {
  return function(text){
     return (''+(text||'')).length;
  }
});

var length = $filter('length');
expect(length(null)).toEqual(0);
expect(length('abc')).toEqual(3);

4. Directives

5. Mocks

6. Global State Isolation

7. Preferred way of Testing

8. JavascriptTestDriver

9. Jasmine

10.   Sample project

 后续继续更新相关文章,谢谢大家对本站的支持!

Javascript 相关文章推荐
javascript 限制输入脚本大全
Nov 03 Javascript
基于jquery的商品展示放大镜
Aug 07 Javascript
jquery实现加载等待效果示例
Sep 25 Javascript
jquery+css实现的红色线条横向二级菜单效果
Aug 22 Javascript
jQuery实现的超简单点赞效果实例分析
Dec 31 Javascript
JavaScript之json_动力节点Java学院整理
Jun 29 Javascript
JavaScript对JSON数据进行排序和搜索
Jul 24 Javascript
angular.extend方法的具体使用
Sep 14 Javascript
解决在vue+webpack开发中出现两个或多个菜单公用一个组件问题
Nov 28 Javascript
从0到1构建vueSSR项目之node以及vue-cli3的配置
Mar 07 Javascript
在vue中对数组值变化的监听与重新响应渲染操作
Jul 17 Javascript
javascript实现点击按钮切换轮播图功能
Sep 23 Javascript
AngularJs Managing Service Dependencies详解
Sep 02 #Javascript
AngularJs Injecting Services Into Controllers详解
Sep 02 #Javascript
AngularJs  Creating Services详解及示例代码
Sep 02 #Javascript
利用jQuery实现打字机字幕效果实例代码
Sep 02 #Javascript
AngularJs  Using $location详解及示例代码
Sep 02 #Javascript
基于JS实现类似支付宝支付密码输入框
Sep 02 #Javascript
JavaScript中Number对象的toFixed() 方法详解
Sep 02 #Javascript
You might like
WordPress开发中自定义菜单的相关PHP函数使用简介
2016/01/05 PHP
PHP实现递归目录的5种方法
2016/10/27 PHP
PHP简单实现遍历目录下特定文件的方法小结
2017/05/22 PHP
jquery form表单提交插件asp.net后台中文解码
2010/06/12 Javascript
javascript制作loading动画效果 loading效果
2014/01/14 Javascript
JS给超链接加确认对话框的方法
2015/02/24 Javascript
jquery实现标签支持图文排列带上下箭头按钮的选项卡
2015/03/14 Javascript
基于BootStrap Metronic开发框架经验小结【一】框架总览及菜单模块的处理
2016/05/12 Javascript
JS通过调用微信API实现微信支付功能的方法示例
2017/06/29 Javascript
Javascript中this关键字指向问题的测试与详解
2017/08/11 Javascript
Angular Material Icon使用详解
2018/11/07 Javascript
微信小程序实现动态显示和隐藏某个控件功能示例
2018/12/14 Javascript
微信小程序列表时间戳转换实现过程解析
2019/10/12 Javascript
vue 父组件通过$refs获取子组件的值和方法详解
2019/11/07 Javascript
js实现圆形菜单选择器
2020/12/03 Javascript
[09:59]DOTA2-DPC中国联赛2月7日Recap集锦
2021/03/11 DOTA
Python struct模块解析
2014/06/12 Python
python使用datetime模块计算各种时间间隔的方法
2015/03/24 Python
Python操作Redis之设置key的过期时间实例代码
2018/01/25 Python
Python爬虫实现获取动态gif格式搞笑图片的方法示例
2018/12/24 Python
django 基于中间件实现限制ip频繁访问过程详解
2019/07/30 Python
python区分不同数据类型的方法
2019/10/14 Python
学生如何注册Pycharm专业版以及pycharm的安装
2020/09/24 Python
美特斯邦威官方商城:邦购网
2016/10/13 全球购物
意大利高端时尚买手店:Stefania Mode
2018/03/01 全球购物
英国领先的野生鸟类食品供应商:GardenBird
2018/08/09 全球购物
Bose英国官方网站:美国知名音响品牌
2020/01/26 全球购物
成人高等教育毕业生自我鉴定
2013/10/22 职场文书
护理专业学生的求职信范文
2013/12/11 职场文书
租车协议书范本2014
2014/11/17 职场文书
优秀员工事迹材料
2014/12/20 职场文书
教师年度考核个人总结
2015/02/12 职场文书
对学校的意见和建议
2015/06/04 职场文书
2015年社区国庆节活动总结
2015/07/30 职场文书
浅谈Go语言多态的实现与interface使用
2021/06/16 Golang
python geopandas读取、创建shapefile文件的方法
2021/06/29 Python