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和php如何获取当前url的内容
Sep 22 Javascript
JS实现登录页面记住密码和enter键登录方法推荐
May 10 Javascript
jQuery中each()、find()和filter()等节点操作方法详解(推荐)
May 25 Javascript
原生JS实现在线问卷调查投票特效
Jan 03 Javascript
ES6下React组件的写法示例代码
May 04 Javascript
jQuery中的类名选择器(.class)用法简单示例
May 14 jQuery
JavaScript中BOM对象原理与用法分析
Jul 09 Javascript
解决Layui中layer报错的问题
Sep 03 Javascript
使用 JavaScript 创建并下载文件(模拟点击)
Oct 25 Javascript
JS数组方法join()用法实例分析
Jan 18 Javascript
openlayers4实现点动态扩散
Aug 17 Javascript
js实现弹幕墙效果
Dec 10 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 模拟post_验证页面的返回状态(实例讲解)
2013/10/28 PHP
php无限遍历目录示例
2014/02/21 PHP
php实现的mongodb操作类实例
2015/04/03 PHP
PHP在innodb引擎下快速代建全文搜索功能简明教程【基于xunsearch】
2016/10/14 PHP
Fastest way to build an HTML string(拼装html字符串的最快方法)
2011/08/20 Javascript
浅谈Jquery为元素绑定事件
2015/04/27 Javascript
javascript实现的简单计时器
2015/07/19 Javascript
JavaScript中的boolean布尔值使用学习及相关技巧讲解
2016/05/26 Javascript
Javascript打印局部页面实例
2016/06/21 Javascript
JS实现iframe自适应高度的方法(兼容IE与FireFox)
2016/06/24 Javascript
基于AngularJS实现iOS8自带的计算器
2016/09/12 Javascript
Bootstrap 3的box-sizing样式导致UEditor控件的图片无法正常缩放的解决方案
2016/09/15 Javascript
bootstrap中添加额外的图标实例代码
2017/02/15 Javascript
xmlplus组件设计系列之列表(4)
2017/04/26 Javascript
Vue组件库发布到npm详解
2018/02/17 Javascript
axios如何取消重复无用的请求详解
2019/12/15 Javascript
解决vue elementUI中table里数字、字母、中文混合排序问题
2020/01/07 Javascript
vue实现全屏滚动效果(非fullpage.js)
2020/03/07 Javascript
vue中是怎样监听数组变化的
2020/10/24 Javascript
[56:18]DOTA2上海特级锦标赛主赛事日 - 4 败者组第四轮#2 MVP.Phx VS Fnatic第二局
2016/03/05 DOTA
[56:46]2018DOTA2亚洲邀请赛 3.31 小组赛 B组 VP vs Effect
2018/04/01 DOTA
用Python的urllib库提交WEB表单
2009/02/24 Python
python根据出生日期返回年龄的方法
2015/03/26 Python
Python画图实现同一结点多个柱状图的示例
2019/07/07 Python
python 有效的括号的实现代码示例
2019/11/11 Python
Python3 main函数使用sys.argv传入多个参数的实现
2019/12/25 Python
Python类型转换的魔术方法详解
2020/12/23 Python
css3 图片圆形显示 如何CSS将正方形图片显示为圆形图片布局
2014/10/10 HTML / CSS
销售自我评价
2013/10/22 职场文书
税务会计岗位职责
2014/02/18 职场文书
推荐信模板
2014/05/09 职场文书
学习优秀党员杨宗兴先进事迹材料思想汇报
2014/09/14 职场文书
初中政治教学反思
2016/02/23 职场文书
Minikube搭建Kubernetes集群
2022/03/31 Servers
CI Games宣布《堕落之王2》使用虚幻引擎5制作 预计将于2023年正式发售
2022/04/11 其他游戏
使用Postman测试需要授权的接口问题
2022/06/21 Java/Android