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 相关文章推荐
用JQuery 实现的自定义对话框
Mar 24 Javascript
js中的布尔运算符使用介绍
Nov 20 Javascript
JS实现当前页居中分页效果的方法
Jun 18 Javascript
JavaScript必知必会(七)js对象继承
Jun 08 Javascript
BootStrap注意事项小结(五)表单
Mar 10 Javascript
详解如何使用webpack打包Vue工程
May 27 Javascript
原生JS写Ajax的请求函数功能
Dec 22 Javascript
详解js正则表达式验证时间格式xxxx-xx-xx形式
Feb 09 Javascript
Vue数据监听方法watch的使用
Mar 28 Javascript
angular将html代码输出为内容的实例
Sep 30 Javascript
基于vue手写tree插件的那点事儿
Aug 20 Javascript
前端性能优化建议
Sep 17 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
php 获取本机外网/公网IP的代码
2010/05/09 PHP
使用array mutisort 实现按某字段对数据排序
2013/06/18 PHP
ThinkPHP CURD方法之where方法详解
2014/06/18 PHP
PHP实现ftp上传文件示例
2014/08/21 PHP
ThinkPHP中的create方法与自动令牌验证实例教程
2014/08/22 PHP
thinkphp使用literal防止模板标签被解析的方法
2014/11/22 PHP
Yii使用EasyWechat实现小程序获取用户的openID的方法
2020/04/29 PHP
JavaScript 序列化对象实现代码
2009/12/18 Javascript
IE6/7/8中Option元素未设value时Select将获取空字符串
2011/04/07 Javascript
jquery插件开发注意事项小结
2013/06/04 Javascript
JavaScript中的style.cssText使用教程
2014/11/06 Javascript
jquery图片切换插件
2015/03/16 Javascript
jQuery实现首页顶部可伸缩广告特效代码
2015/04/15 Javascript
JavaScript采用递归算法计算阶乘实例
2015/08/04 Javascript
JS实现的点击表头排序功能示例
2017/03/27 Javascript
Bootstrap Multiselect 常用组件实现代码
2017/07/09 Javascript
zTree异步加载展开第一级节点的实现方法
2017/09/05 Javascript
vue父组件向子组件动态传值的两种方法
2017/11/11 Javascript
浅析JavaScript中的特殊数据类型
2017/12/15 Javascript
使用async-validator编写Form组件的方法
2018/01/10 Javascript
一个因@click.stop引发的bug的解决
2019/01/08 Javascript
2019 年编写现代 JavaScript 代码的5个小技巧(小结)
2019/01/15 Javascript
浅谈JS中几种轻松处理'this'指向方式
2019/09/16 Javascript
详解vite2.0配置学习(typescript版本)
2021/02/25 Javascript
[02:59]DOTA2完美大师赛主赛事第三日精彩集锦
2017/11/25 DOTA
Python3实现的腾讯微博自动发帖小工具
2013/11/11 Python
python使用电子邮件模块smtplib的方法
2016/08/28 Python
Python 实现链表实例代码
2017/04/07 Python
解决csv.writer写入文件有多余的空行问题
2018/07/06 Python
Python sorted函数详解(高级篇)
2018/09/18 Python
解决pycharm启动后总是不停的updating indices...indexing的问题
2019/11/27 Python
一文弄懂Pytorch的DataLoader, DataSet, Sampler之间的关系
2020/07/03 Python
Python爬取数据并实现可视化代码解析
2020/08/12 Python
安全教育培训心得体会
2016/01/15 职场文书
使用这 6个Vue加载动画库来减少我们网站的跳出率
2021/05/18 Vue.js
MySQL远程无法连接的一些常见原因总结
2022/09/23 MySQL