JavaScript单元测试ABC

 更新时间:2012年04月12日 12:07:05   作者:  
在服务器端的单元测试中,都有各种各样的测试框架,在JavaScript中现在也有一些很优秀的框架,但在本文中,我们将自己动手一步步来实现一个简单的单元测试框架
前言
  当前,在软件开发中单元测试越来越受到开发者的重视,它能提高软件的开发效率,而且能保障开发的质量。以往,单元测试往往多见于服务端的开发中,但随着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);
}

  运行后会弹出如下窗口:

第三版:进行批量测试

  在第二版中,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");
}
});

  结果为:

这样我们一眼就可以看出哪个出错了。但这样是否就完美了呢,我们可以看到最后那个测试中 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

相关文章

  • js类中获取外部函数名的方法

    js类中获取外部函数名的方法

    js类中获取外部函数名的方法...
    2007-08-08
  • JavaScript新增的两个原始数据类型详解(Record和Tuple)

    JavaScript新增的两个原始数据类型详解(Record和Tuple)

    js中原始数据类型有Number、String、Boolean、Null、Undefined、Symbol (ES6引入的数据类型),引用的数据类型有Object,Array,Function,下面这篇文章主要给大家介绍了关于JavaScript新增两个原始数据类型(Record和Tuple)的相关资料,需要的朋友可以参考下
    2022-03-03
  • 原生javascript实现addClass,removeClass,hasClass函数

    原生javascript实现addClass,removeClass,hasClass函数

    这篇文章主要介绍了原生javascript实现addClass,removeClass,hasClass函数的相关代码,有需要的小伙伴可以参考下
    2016-02-02
  • layui实现三级导航菜单

    layui实现三级导航菜单

    这篇文章主要为大家详细介绍了layui实现三级导航菜单,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-07-07
  • javascript获取URL参数与参数值的示例代码

    javascript获取URL参数与参数值的示例代码

    本篇文章主要是对javascript获取URL参数与参数值的示例代码进行了介绍,需要的朋友可以过来参考下,希望对大家有所帮助
    2013-12-12
  • JavaScript垃圾回收机制原理总结深入探究

    JavaScript垃圾回收机制原理总结深入探究

    就像人类会产生垃圾一样,程序运行过程中也会产生垃圾,如果不及时回收轻则将会拖慢程序运行,重则会导致系统崩溃,也就是所谓的内存泄漏。所以垃圾回收非常必要
    2022-10-10
  • 微信h5静默和非静默授权获取用户openId的方法和步骤

    微信h5静默和非静默授权获取用户openId的方法和步骤

    这篇文章主要介绍了微信h5静默和非静默授权获取用户openId的方法和步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-06-06
  • JavaScript编制留言簿程序代码

    JavaScript编制留言簿程序代码

    也许你有一个很酷的主页,很希望与人分享。同时,你希望访问你主页的人能对你的主页提供一些意见和建议,或者你对某个主题感兴趣,而希望客人也许能给你一帮助,这就要用到留言簿。留言簿使得你能与每一个访问你主页的人交换信息。
    2008-09-09
  • JavaScript定义函数的三种实现方法

    JavaScript定义函数的三种实现方法

    这篇文章主要介绍了JavaScript定义函数的三种实现方法的相关资料,希望通过本文大家能够掌握三种定义函数的方法,需要的朋友可以参考下
    2017-09-09
  • JS实现斐波那契数列的五种方式(小结)

    JS实现斐波那契数列的五种方式(小结)

    这篇文章主要介绍了JS实现斐波那契数列的五种方式(小结),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-09-09

最新评论