JavaScript单元测试ABC


Posted in Javascript onApril 12, 2012

前言

当前,在软件开发中单元测试越来越受到开发者的重视,它能提高软件的开发效率,而且能保障开发的质量。以往,单元测试往往多见于服务端的开发中,但随着Web编程领域的分工逐渐明细,在前端Javascript开发领域中,也可以进行相关的单元测试,以保障前端开发的质量。

在服务器端的单元测试中,都有各种各样的测试框架,在JavaScript中现在也有一些很优秀的框架,但在本文中,我们将自己动手一步步来实现一个简单的单元测试框架。

JS单元测试有很多方面,比较多的是对方法功能检查,对浏览器兼容性检查,本文主要谈第一种。

本文检查的JS代码是我以前写的一个JS日期格式化的方法,原文在这里(javascript日期格式化函数,跟C#中的使用方法类似),代码如下:

Date.prototype.toString=function(format){ 
var time={}; 
time.Year=this.getFullYear(); 
time.TYear=(""+time.Year).substr(2); 
time.Month=this.getMonth()+1; 
time.TMonth=time.Month<10?"0"+time.Month:time.Month; 
time.Day=this.getDate(); 
time.TDay=time.Day<10?"0"+time.Day:time.Day; 
time.Hour=this.getHours(); 
time.THour=time.Hour<10?"0"+time.Hour:time.Hour; 
time.hour=time.Hour<13?time.Hour:time.Hour-12; 
time.Thour=time.hour<10?"0"+time.hour:time.hour; 
time.Minute=this.getMinutes(); 
time.TMinute=time.Minute<10?"0"+time.Minute:time.Minute; 
time.Second=this.getSeconds(); 
time.TSecond=time.Second<10?"0"+time.Second:time.Second; 
time.Millisecond=this.getMilliseconds(); 
var oNumber=time.Millisecond/1000; 
if(format!=undefined && format.replace(/\s/g,"").length>0){ 
format=format 
.replace(/yyyy/ig,time.Year) 
.replace(/yyy/ig,time.Year) 
.replace(/yy/ig,time.TYear) 
.replace(/y/ig,time.TYear) 
.replace(/MM/g,time.TMonth) 
.replace(/M/g,time.Month) 
.replace(/dd/ig,time.TDay) 
.replace(/d/ig,time.Day) 
.replace(/HH/g,time.THour) 
.replace(/H/g,time.Hour) 
.replace(/hh/g,time.Thour) 
.replace(/h/g,time.hour) 
.replace(/mm/g,time.TMinute) 
.replace(/m/g,time.Minute) 
.replace(/ss/ig,time.TSecond) 
.replace(/s/ig,time.Second) 
.replace(/fff/ig,time.Millisecond) 
.replace(/ff/ig,oNumber.toFixed(2)*100) 
.replace(/f/ig,oNumber.toFixed(1)*10); 
} 
else{ 
format=time.Year+"-"+time.Month+"-"+time.Day+" "+time.Hour+":"+time.Minute+":"+time.Second; 
} 
return format; 
}

这段代码目前没有发现比较严重的bug,本文为了测试,我们把 .replace(/MM/g,time.TMonth) 改为 .replace(/MM/g,time.Month),这个错误是当月份小于10时,没有用两位数表示月份。

现在有这么一句话,好的设计都是重构出来的,在本文中也一样,我们从最简单的开始。
第一版:用最原始的alert

作为第一版,我们很偷懒的直接用alert来检查,完整代码如下:

<!DOCTYPE html> 
<html> 
<head> 
<title>Demo</title> 
<meta charset="utf-8"/> 
</head> 
<body> 
<script type="text/javascript"> 
Date.prototype.toString=function(format){ 
var time={}; 
time.Year=this.getFullYear(); 
time.TYear=(""+time.Year).substr(2); 
time.Month=this.getMonth()+1; 
time.TMonth=time.Month<10?"0"+time.Month:time.Month; 
time.Day=this.getDate(); 
time.TDay=time.Day<10?"0"+time.Day:time.Day; 
time.Hour=this.getHours(); 
time.THour=time.Hour<10?"0"+time.Hour:time.Hour; 
time.hour=time.Hour<13?time.Hour:time.Hour-12; 
time.Thour=time.hour<10?"0"+time.hour:time.hour; 
time.Minute=this.getMinutes(); 
time.TMinute=time.Minute<10?"0"+time.Minute:time.Minute; 
time.Second=this.getSeconds(); 
time.TSecond=time.Second<10?"0"+time.Second:time.Second; 
time.Millisecond=this.getMilliseconds(); 
var oNumber=time.Millisecond/1000; 
if(format!=undefined && format.replace(/\s/g,"").length>0){ 
format=format 
.replace(/yyyy/ig,time.Year) 
.replace(/yyy/ig,time.Year) 
.replace(/yy/ig,time.TYear) 
.replace(/y/ig,time.TYear) 
.replace(/MM/g,time.Month) 
.replace(/M/g,time.Month) 
.replace(/dd/ig,time.TDay) 
.replace(/d/ig,time.Day) 
.replace(/HH/g,time.THour) 
.replace(/H/g,time.Hour) 
.replace(/hh/g,time.Thour) 
.replace(/h/g,time.hour) 
.replace(/mm/g,time.TMinute) 
.replace(/m/g,time.Minute) 
.replace(/ss/ig,time.TSecond) 
.replace(/s/ig,time.Second) 
.replace(/fff/ig,time.Millisecond) 
.replace(/ff/ig,oNumber.toFixed(2)*100) 
.replace(/f/ig,oNumber.toFixed(1)*10); 
} 
else{ 
format=time.Year+"-"+time.Month+"-"+time.Day+" "+time.Hour+":"+time.Minute+":"+time.Second; 
} 
return format; 
} 
var date=new Date(2012,3,9); 
alert(date.toString("yyyy")); 
alert(date.toString("MM")); 
</script> 
</body> 
</html>

运行后会弹出 2012 和 4 ,观察结果我们知道 date.toString("MM")方法是有问题的。

这种方式很不方便,最大的问题是它只弹出了结果,并没有给出正确或错误的信息,除非对代码非常熟悉,否则很难知道弹出的结果是正是误,下面,我们写一个断言(assert)方法来进行测试,明确给出是正是误的信息。
第二版:用assert进行检查

断言是表达程序设计人员对于系统应该达到状态的一种预期,比如有一个方法用于把两个数字加起来,对于3+2,我们预期这个方法返回的结果是5,如果确实返回5那么就通过,否则给出错误提示。

断言是单元测试的核心,在各种单元测试的框架中都提供了断言功能,这里我们写一个简单的断言(assert)方法:

function assert(message,result){ 
if(!result){ 
throw new Error(message); 
} 
return true; 
}

这个方法接受两个参数,第一个是错误后的提示信息,第二个是断言结果

用断言测试代码如下:

var date=new Date(2012,3,9); 
try{ 
assert("yyyy should return full year",date.toString("yyyy")==="2012"); 
}catch(e){ 
alert("Test failed:"+e.message); 
} 
try{ 
assert("MM should return full month",date.toString("MM")==="04"); 
} 
catch(e){ 
alert("Test failed:"+e.message); 
}

运行后会弹出如下窗口:

JavaScript单元测试ABC

第三版:进行批量测试

在第二版中,assert方法可以给出明确的结果,但如果想进行一系列的测试,每个测试都要进行异常捕获,还是不够方便。另外,在一般的测试框架中都可以给出成功的个数,失败的个数,及失败的错误信息。

为了可以方便在看到测试结果,这里我们把结果用有颜色的文字显示的页面上,所以这里要写一个小的输出方法PrintMessage:

function PrintMessage(text,color){ 
var div=document.createElement("div"); 
div.innerHTML=text; 
div.style.color=color; 
document.body.appendChild(div); 
delete div; 
}

下面,我们就写一个类似jsTestDriver中的TestCase方法,来进行批量测试:

function testCase(name,tests){ 
var successCount=0; 
var testCount=0; 
for(var test in tests){ 
testCount++; 
try{ 
tests[test](); 
PrintMessage(test+" success","#080"); 
successCount++; 
} 
catch(e){ 
PrintMessage(test+" failed:"+e.message,"#800"); 
} 
} 
PrintMessage("Test result: "+testCount+" tests,"+successCount+" success, "+ (testCount-successCount)+" failures","#800"); 
}

测试代码:

var date=new Date(2012,3,9); 
testCase("date toString test",{ 
yyyy:function(){ 
assert("yyyy should return 2012",date.toString("yyyy")==="2012"); 
}, 
MM:function(){ 
assert("MM should return 04",date.toString("MM")==="04"); 
}, 
dd:function(){ 
assert("dd should return 09",date.toString("dd")==="09"); 
} 
});

结果为:

JavaScript单元测试ABC

这样我们一眼就可以看出哪个出错了。但这样是否就完美了呢,我们可以看到最后那个测试中 var date=new Date(2012,3,9)是放在testCase外面定义的,并且整个testCase的测试代码中共用了date,这里因为各个方法中没有对date的值进行修改,所以没出问题,如果某个测试方法中对date的值修改了呢,测试的结果就是不准确的,所以在很多测试框架中都提供了setUp和tearDown方法,用来对统一提供和销毁测试数据,下面我们就在我们的testCase中加上setUp和tearDown方法。
第四版:统一提供测试数据的批量测试

首先我们添加setUp和tearDown方法:

testCase("date toString",{ 
setUp:function(){ 
this.date=new Date(2012,3,9); 
}, 
tearDown:function(){ 
delete this.date; 
}, 
yyyy:function(){ 
assert("yyyy should return 2012",this.date.toString("yyyy")==="2012"); 
}, 
MM:function(){ 
assert("MM should return 04",this.date.toString("MM")==="04"); 
}, 
dd:function(){ 
assert("dd should return 09",this.date.toString("dd")==="09"); 
} 
});

由于setUp和tearDown方法不参与测试,所以我们要修改testCase代码:

function testCase(name,tests){ 
var successCount=0; 
var testCount=0; 
var hasSetUp=typeof tests.setUp == "function"; 
var hasTearDown=typeof tests.tearDown == "function"; 
for(var test in tests){ 
if(test==="setUp"||test==="tearDown"){ 
continue; 
} 
testCount++; 
try{ 
if(hasSetUp){ 
tests.setUp(); 
} 
tests[test](); 
PrintMessage(test+" success","#080"); if(hasTearDown){ 
tests.tearDown(); 
} 
successCount++; 
} 
catch(e){ 
PrintMessage(test+" failed:"+e.message,"#800"); 
} 
} 
PrintMessage("Test result: "+testCount+" tests,"+successCount+" success, "+ (testCount-successCount)+" failures","#800"); 
}

运行后的结果跟第三版相同。
小结及参考文章

上面说了,好的设计是不断重构的结果,上面的第四版是不是就完美了呢,远远没有达到,这里只是一个示例。如果大家需要这方面的知识,我后面可以再写写各个测试框架的使用。

本文只是JS单元测试入门级的示例,让初学者对JS的单元测试有个初步概念,属于抛砖引玉,欢迎各位高人拍砖补充。

本文参考了《测试驱动的JavaScript开发》(个人觉得还不错,推荐下)一书第一章,书中的测试用例也是一个时间函数,不过写的比较复杂,初学者不太容易看懂。
作者:Artwl

Javascript 相关文章推荐
SUN的《AJAX与J2EE》全文译了
Feb 23 Javascript
Javascript 作用域使用说明
Aug 13 Javascript
JS获取父节点方法
Aug 20 Javascript
Jquery性能优化详解
May 15 Javascript
require.js的用法详解
Oct 20 Javascript
Jquery插件仿百度搜索关键字自动匹配功能
May 11 Javascript
jQuery插件制作的实例教程
May 16 Javascript
轻松掌握JavaScript策略模式
Aug 25 Javascript
django简单的前后端分离的数据传输实例 axios
May 18 Javascript
js 将多个对象合并成一个对象 assign方法的实现
Sep 24 Javascript
在Vue中使用Select选择器拼接label的操作
Oct 22 Javascript
Vue如何实现组件间通信
May 15 Vue.js
扩展JavaScript功能的正确方法(译文)
Apr 12 #Javascript
idTabs基于JQuery的根据URL参数选择Tab插件
Apr 11 #Javascript
JQuery学习笔录 简单的JQuery
Apr 09 #Javascript
广泛收集的jQuery拖放插件集合
Apr 09 #Javascript
深入分析js中的constructor和prototype
Apr 07 #Javascript
浅谈javascript中的作用域
Apr 07 #Javascript
JavaScript 高级篇之DOM文档,简单封装及调用、动态添加、删除样式(六)
Apr 07 #Javascript
You might like
ThinkPHP3.1新特性之内容解析输出详解
2014/06/19 PHP
php随机取mysql记录方法小结
2014/12/27 PHP
利用PHP判断文件是否为图片的方法总结
2017/01/06 PHP
PHP autoload使用方法及步骤详解
2020/09/05 PHP
兼容Mozilla必须知道的知识。
2007/01/09 Javascript
JavaScript网页制作特殊效果用随机数
2007/05/22 Javascript
JQuery 学习笔记 选择器之一
2009/07/23 Javascript
js以对象为索引的关联数组
2010/07/04 Javascript
Js中获取frames中的元素示例代码
2013/07/30 Javascript
利用js动态添加删除table行的示例代码
2013/12/16 Javascript
jQuery实现HTML5 placeholder效果实例
2014/12/09 Javascript
jquery实现很酷的网页顶部图标下拉菜单效果
2015/08/22 Javascript
javascript实现很浪漫的气泡冒出特效
2020/09/05 Javascript
浅谈js数组和splice的用法
2016/12/04 Javascript
jquery二级目录选中当前页的css样式
2016/12/08 Javascript
vue展示dicom文件医疗系统的实现代码
2018/08/27 Javascript
JavaScript创建防篡改对象的方法分析
2018/12/30 Javascript
关于JS解构的5种有趣用法
2019/09/05 Javascript
[49:54]Ti4 循环赛第三日 LGD vs Titan
2014/07/12 DOTA
[03:12]TI9战队档案 - Virtus Pro
2019/08/20 DOTA
python简单实现计算过期时间的方法
2015/06/09 Python
python写日志封装类实例
2015/06/28 Python
Python中二维列表如何获取子区域元素的组成
2017/01/19 Python
Python中的错误和异常处理简单操作示例【try-except用法】
2017/07/25 Python
python实现简单名片管理系统
2018/11/30 Python
Python设计模式之观察者模式原理与用法详解
2019/01/16 Python
python代码 FTP备份交换机配置脚本实例解析
2019/08/01 Python
Django项目主urls导入应用中views的红线问题解决
2019/08/10 Python
pytorch动态网络以及权重共享实例
2020/01/06 Python
基于Tensorflow使用CPU而不用GPU问题的解决
2020/02/07 Python
英国领先的办公用品供应商:Viking
2016/08/01 全球购物
人力资源管理毕业生自荐信
2013/11/21 职场文书
商务邀请函范文
2014/01/14 职场文书
《手指教学》反思
2014/02/14 职场文书
大四优秀党员个人民主评议
2014/09/19 职场文书
小学教师年度个人总结
2015/02/05 职场文书