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 相关文章推荐
html读出文本文件内容
Jan 22 Javascript
js二级地域选择的实现方法
Jun 17 Javascript
js实现的点击数量加一可操作数据库
May 09 Javascript
jQuery表格排序组件-tablesorter使用示例
May 26 Javascript
jquery实现通用版鼠标经过淡入淡出效果
Jun 15 Javascript
JS交换变量的方法
Jan 21 Javascript
浅谈Node.js之异步流控制
Oct 25 Javascript
微信小程序实现点击效果
Jun 21 Javascript
弱类型语言javascript开发中的一些坑实例小结【变量、函数、数组、对象、作用域等】
Aug 07 Javascript
React传值 组件传值 之间的关系详解
Aug 26 Javascript
Weex开发之地图篇的具体使用
Oct 16 Javascript
Vue2.0 $set()的正确使用详解
Jul 28 Javascript
扩展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
php+ajax实现无刷新动态加载数据技术
2015/04/28 PHP
typecho插件编写教程(四):插件挂载
2015/05/28 PHP
PHP变量赋值、代入给JavaScript中的变量
2015/06/29 PHP
laravel5.4利用163邮箱发送邮件的步骤详解
2017/09/22 PHP
JavaScript窗口功能指南之在窗口中书写内容
2006/07/21 Javascript
jquery.validate使用攻略 第三部
2010/07/01 Javascript
javascript实现div浮动在网页最顶上并带关闭按钮效果实例
2013/08/13 Javascript
详解JS 比较两个Json对象的值是否相等的实例
2013/11/20 Javascript
探讨js字符串数组拼接的性能问题
2014/10/11 Javascript
理解Angular数据双向绑定
2016/01/10 Javascript
原生js实现可拖拽效果
2017/02/28 Javascript
angular框架实现全选与单选chekbox的自定义
2017/07/06 Javascript
layui点击按钮页面会自动刷新的解决方案
2019/10/25 Javascript
Vue el-autocomplete远程搜索下拉框并实现自动填充功能(推荐)
2019/10/25 Javascript
关于Vue中$refs的探索浅析
2020/11/05 Javascript
[04:16]DOTA2全国高校联赛16强抽签
2018/05/02 DOTA
python数据结构之二叉树的统计与转换实例
2014/04/29 Python
Python中一些不为人知的基础技巧总结
2018/05/19 Python
django利用request id便于定位及给日志加上request_id
2018/08/26 Python
PyTorch实现更新部分网络,其他不更新
2019/12/31 Python
Python 面向对象之类class和对象基本用法示例
2020/02/02 Python
Application Cache未缓存文件无法访问无法加载问题
2014/05/31 HTML / CSS
美国在线奢侈品寄售商店:Luxury Garage Sale
2018/08/19 全球购物
澳大利亚二手奢侈品网站:Modsie
2019/09/23 全球购物
什么是跨站脚本攻击
2014/12/11 面试题
军校制空专业毕业生自我鉴定
2013/11/16 职场文书
妇产医师自荐信
2014/01/29 职场文书
党的群众路线教育实践活动批评与自我批评
2014/02/16 职场文书
探亲假请假条
2014/04/11 职场文书
2014年大学生职业规划书:未来不是梦,只要勇敢冲!
2014/09/22 职场文书
幼儿园中班教师个人工作总结
2015/02/06 职场文书
中学生自我评价范文
2015/03/03 职场文书
2015年乡镇人大工作总结
2015/04/22 职场文书
2016年国庆节宣传标语
2015/11/25 职场文书
关于感恩老师的古诗句
2019/08/20 职场文书
一篇文章弄懂Python中的内建函数
2021/08/07 Python