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 相关文章推荐
JS获取scrollHeight问题想到的标准问题
May 27 Javascript
网页中的图片的处理方法与代码
Nov 26 Javascript
JQuery循环滚动图片代码
Dec 08 Javascript
arguments对象验证函数的参数是否合法
Jun 26 Javascript
AngularJS通过ng-route实现基本的路由功能实例详解
Dec 13 Javascript
vue-hook-form使用详解
Apr 07 Javascript
jQuery日期范围选择器附源码下载
May 23 jQuery
javascript数组定义的几种方法
Oct 06 Javascript
微信小程序实现发红包功能
Jul 11 Javascript
Vue开发Html5微信公众号的步骤
Apr 11 Javascript
js实现经典贪吃蛇小游戏
Mar 19 Javascript
JavaScript实现两个数组的交集
Mar 25 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
开源SNS系统-ThinkSNS
2008/05/18 PHP
PHP Header用于页面跳转要注意的几个问题总结
2008/10/03 PHP
举例详解PHP脚本的测试方法
2015/08/05 PHP
实例讲解php数据访问
2016/05/09 PHP
使用php从身份证号中获取一系列线索(星座、生肖、生日等)
2016/05/11 PHP
PHP基于双向链表与排序操作实现的会员排名功能示例
2017/12/26 PHP
PHP+Ajax实现上传文件进度条动态显示进度功能
2018/06/04 PHP
js实现具有高亮显示效果的多级菜单代码
2015/09/01 Javascript
基于javascript实现简单计算器功能
2016/01/03 Javascript
BootStrap 智能表单实战系列(二)BootStrap支持的类型简介
2016/06/13 Javascript
jQuery设置单选按钮radio选中/不可用的实例代码
2016/06/24 Javascript
JS判断输入字符串长度实例代码(汉字算两个字符,字母数字算一个)
2016/08/02 Javascript
详解使用路由延迟加载 Angular 模块
2017/10/12 Javascript
Vue仿今日头条实例详解
2018/02/06 Javascript
vue数组对象排序的实现代码
2018/06/20 Javascript
详解easyui基于 layui.laydate日期扩展组件
2018/07/18 Javascript
详解JS中统计函数执行次数与执行时间
2018/09/04 Javascript
Vue.js上传图片到阿里云OSS存储的方法示例
2018/12/13 Javascript
基于JavaScript 实现拖放功能
2019/09/12 Javascript
通过js实现压缩图片上传功能
2020/02/25 Javascript
解决Vue router-link绑定事件不生效的问题
2020/07/22 Javascript
在Vue中获取自定义属性方法:data-id的实例
2020/09/09 Javascript
Vue页面跳转传递参数及接收方式
2020/09/09 Javascript
[02:23]DOTA2英雄基础教程 幻影长矛手
2013/12/09 DOTA
python安装教程 Pycharm安装详细教程
2017/05/02 Python
Python 类属性与实例属性,类对象与实例对象用法分析
2019/09/20 Python
Python loguru日志库之高效输出控制台日志和日志记录
2020/03/07 Python
python属于哪种语言
2020/08/16 Python
移动web模拟客户端实现多方框输入密码效果【附代码】
2016/03/25 HTML / CSS
英国皇家邮政海外旗舰店:Royal Mail
2018/02/21 全球购物
西铁城美国官方网站:Citizen Watch美国
2019/11/08 全球购物
关于感恩的演讲稿500字
2014/08/26 职场文书
乡镇党委书记个人整改措施
2014/09/15 职场文书
研究生论文答辩开场白
2015/05/27 职场文书
浅谈MySQL之浅入深出页原理
2021/06/23 MySQL
Win11 Beta 预览版 22621.575 和 22622.575更新补丁KB5016694发布(附更新内容大全)
2022/08/14 数码科技