惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

A
About on SuperTechFans
C
Cybersecurity and Infrastructure Security Agency CISA
N
News and Events Feed by Topic
C
Cisco Blogs
Cisco Talos Blog
Cisco Talos Blog
A
Arctic Wolf
Scott Helme
Scott Helme
P
Palo Alto Networks Blog
S
Schneier on Security
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tor Project blog
量子位
G
Google Developers Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
B
Blog RSS Feed
NISL@THU
NISL@THU
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
AWS News Blog
AWS News Blog
爱范儿
爱范儿
Last Week in AI
Last Week in AI
Y
Y Combinator Blog
L
LINUX DO - 最新话题
Security Archives - TechRepublic
Security Archives - TechRepublic
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Secure Thoughts
Cloudbric
Cloudbric
aimingoo的专栏
aimingoo的专栏
L
Lohrmann on Cybersecurity
TaoSecurity Blog
TaoSecurity Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Hacker News: Ask HN
Hacker News: Ask HN
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The GitHub Blog
The GitHub Blog
有赞技术团队
有赞技术团队
S
Security @ Cisco Blogs
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Cyber Attacks, Cyber Crime and Cyber Security
G
GRAHAM CLULEY
P
Proofpoint News Feed
V
V2EX
Martin Fowler
Martin Fowler
C
CERT Recently Published Vulnerability Notes
Attack and Defense Labs
Attack and Defense Labs
C
CXSECURITY Database RSS Feed - CXSecurity.com
The Cloudflare Blog
SecWiki News
SecWiki News
罗磊的独立博客
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
小众软件
小众软件
The Last Watchdog
The Last Watchdog

张鑫旭-鑫空间-鑫生活

1分钟速度了解text-align match-parent声明 « 张鑫旭-鑫空间-鑫生活 光标的形状也能设置了,就是CSS caret-shape属性 « 张鑫旭-鑫空间-鑫生活 pretext与文字四面环绕效果的实现 « 张鑫旭-鑫空间-鑫生活 JSON.rawJSON方法的作用是什么? « 张鑫旭-鑫空间-鑫生活 如何使用CSS判断鼠标从哪个方向进入元素? « 张鑫旭-鑫空间-鑫生活 借助mediabunny纯JS实现视频水印、剪裁、合成等功能 « 张鑫旭-鑫空间-鑫生活 务必谨慎使用JS WeakRef弱引用 « 张鑫旭-鑫空间-鑫生活 CSS六边形头像的实现与蜂巢布局 « 张鑫旭-鑫空间-鑫生活 CSS corner-shape与背景底纹技术 « 张鑫旭-鑫空间-鑫生活 浅学WebTransport API:下一代Web双向通信技术 « 张鑫旭-鑫空间-鑫生活 HTML dialog元素新支持了closedBy属性 « 张鑫旭-鑫空间-鑫生活 HTML interestfor属性与悬停popover交互效果 « 张鑫旭-鑫空间-鑫生活 CSS3模拟window7炫酷界面效果展示 « 张鑫旭-鑫空间-鑫生活 图片旋转+剪裁js插件(兼容各浏览器) « 张鑫旭-鑫空间-鑫生活 纯js无图片无外链CSS图片幻灯片播放插件 « 张鑫旭-鑫空间-鑫生活 CSS border三角、圆角图形生成技术简介 « 张鑫旭-鑫空间-鑫生活 CSS “渐进增强”在web制作中常见应用举例 « 张鑫旭-鑫空间-鑫生活 让IE6/IE7/IE8浏览器支持CSS3属性 « 张鑫旭-鑫空间-鑫生活 jQuery – 高亮动态显示页面HTML代码插件 « 张鑫旭-鑫空间-鑫生活 翻译 – 逐渐消失的Flash网站 « 张鑫旭-鑫空间-鑫生活 CSS reset的重新审视 – 避免样式重置 « 张鑫旭-鑫空间-鑫生活 新浪微博插入话题后部分文字选中的js实现 « 张鑫旭-鑫空间-鑫生活 JavaScript实现http地址自动检测并添加URL链接 « 张鑫旭-鑫空间-鑫生活 CSS实现兼容性的渐变背景(gradient)效果 « 张鑫旭-鑫空间-鑫生活 CSS content内容生成技术以及应用 « 张鑫旭-鑫空间-鑫生活 CSS gradient渐变之webkit核心浏览器下的使用 « 张鑫旭-鑫空间-鑫生活 CSS渐变之CSS3 gradient在Firefox3.6下的使用 « 张鑫旭-鑫空间-鑫生活 区分IE8/IE7/IE6及其他浏览器-CSS “\9″hack « 张鑫旭-鑫空间-鑫生活 CSS实现跨浏览器兼容性的盒阴影效果 « 张鑫旭-鑫空间-鑫生活 CSS3&HTML5各浏览器支持情况一览表 « 张鑫旭-鑫空间-鑫生活 翻译 – CSS Sprites:实用技术还是生厌之物? « 张鑫旭-鑫空间-鑫生活 纯CSS实现易拉罐3D滚动效果 « 张鑫旭-鑫空间-鑫生活 纯CSS实现侧边栏/分栏高度自动相等 « 张鑫旭-鑫空间-鑫生活 CSS+JavaScript实现页面不同布局的切换 « 张鑫旭-鑫空间-鑫生活 JavaScript实现最简单的拖拽效果 « 张鑫旭-鑫空间-鑫生活 自己写的无图片版jQuery zxxbox弹出框插件 « 张鑫旭-鑫空间-鑫生活 中国古代道家思想与网页重构的思考 « 张鑫旭-鑫空间-鑫生活 让所有浏览器支持HTML5 video视频标签 « 张鑫旭-鑫空间-鑫生活 纯CSS实现各类气球泡泡对话框效果 « 张鑫旭-鑫空间-鑫生活 JS HEX十六进制与RGB, HSL颜色的相互转换 « 张鑫旭-鑫空间-鑫生活 CSS实现兼容性的渐变、高光等文字效果 « 张鑫旭-鑫空间-鑫生活 去除冗余 – 精简您的CSS样式代码 « 张鑫旭-鑫空间-鑫生活 对overflow与zoom”清除浮动”的一些认识 « 张鑫旭-鑫空间-鑫生活 jQuery Pagination Ajax分页插件中文详解 « 张鑫旭-鑫空间-鑫生活 absolute绝对定位的非绝对定位用法 « 张鑫旭-鑫空间-鑫生活 网页布局思想浅议-淘宝新版首页为实例 « 张鑫旭-鑫空间-鑫生活 回流与重绘:CSS性能让JavaScript变慢? « 张鑫旭-鑫空间-鑫生活 CSS float浮动的深入研究、详解及拓展(二) « 张鑫旭-鑫空间-鑫生活 CSS float浮动的深入研究、详解及拓展(一) « 张鑫旭-鑫空间-鑫生活 60个极富创造性的震撼的电影海报设计 « 张鑫旭-鑫空间-鑫生活 jQuery Lightbox(balupton版)图片展示插件讲解 « 张鑫旭-鑫空间-鑫生活 博客内嵌flash音乐播放器代码、下载与使用 « 张鑫旭-鑫空间-鑫生活 CSS3 animate实现图片墙3D翻转效果 « 张鑫旭-鑫空间-鑫生活 图片旋转效果的一些研究、jQuery插件及实例 « 张鑫旭-鑫空间-鑫生活 CSS3 border-image详解、应用及jQuery插件 « 张鑫旭-鑫空间-鑫生活 jQuery-两款不同原理的圆角插件讲解 « 张鑫旭-鑫空间-鑫生活 页面可用性之outline轮廓外框的一些研究 « 张鑫旭-鑫空间-鑫生活 CSS3 transition实现超酷图片墙动画效果 « 张鑫旭-鑫空间-鑫生活 HTML CSS列表元素ul,ol,dl的研究与应用 « 张鑫旭-鑫空间-鑫生活 jQuery图片文本滚动切换插件jCarousel中文翻译与详解 « 张鑫旭-鑫空间-鑫生活 酷酷的jQuery鼠标悬停图片放大切换显示效果 « 张鑫旭-鑫空间-鑫生活 IE6下z-index犯癫不起作用bug的初步研究 « 张鑫旭-鑫空间-鑫生活 checkbox复选框的一些深入研究与理解 « 张鑫旭-鑫空间-鑫生活 jQuery之使用slideToggle实现垂直下拉菜单 « 张鑫旭-鑫空间-鑫生活 jQuery-innerfade内部列表自动淡入淡出插件 « 张鑫旭-鑫空间-鑫生活 jQuery-火焰灯效果导航菜单 « 张鑫旭-鑫空间-鑫生活 IE下css bug集合-翻译自haslayout.net « 张鑫旭-鑫空间-鑫生活 翻译-IE7/8@font-face嵌入字体与文字平滑 « 张鑫旭-鑫空间-鑫生活 jQuery-马化腾产品设计与用户体验的一些技术实现 « 张鑫旭-鑫空间-鑫生活 像素的世界及其在web开发制作中的应用 « 张鑫旭-鑫空间-鑫生活 css行高line-height的一些深入理解及应用 « 张鑫旭-鑫空间-鑫生活 CSS实现圆角六色渐变自适应按钮详解 « 张鑫旭-鑫空间-鑫生活 我熟知的三种三栏网页宽度自适应布局方法 « 张鑫旭-鑫空间-鑫生活 jQuery照片图像剪裁插件Jcrop中文翻译详解 « 张鑫旭-鑫空间-鑫生活 Ajax Upload多文件上传插件翻译及中文演示 « 张鑫旭-鑫空间-鑫生活 告别图片—使用字符实现兼容性的圆角尖角效果beta版 « 张鑫旭-鑫空间-鑫生活 大小不固定的图片、多行文字的水平垂直居中 « 张鑫旭-鑫空间-鑫生活 jQuery boxy弹出层对话框插件中文演示及讲解 « 张鑫旭-鑫空间-鑫生活 JavaScript实现新浪微博文字放大显示动画效果 « 张鑫旭-鑫空间-鑫生活 翻译:让网络更快一些——最小化浏览器中的回流(reflow) « 张鑫旭-鑫空间-鑫生活 jQuery之addClasas与removeClass使用实例 « 张鑫旭-鑫空间-鑫生活 关于Google圆角高光高宽自适应按钮及其拓展 « 张鑫旭-鑫空间-鑫生活 JavaScript实现图片幻灯片滚动播放动画效果 « 张鑫旭-鑫空间-鑫生活 jQuery-鼠标经过显示大图并跟随鼠标效果插件 « 张鑫旭-鑫空间-鑫生活 关于gif图片(或png8)杂边锯齿的问题 « 张鑫旭-鑫空间-鑫生活 对html与body的一些研究与理解 « 张鑫旭-鑫空间-鑫生活 鲜为人知的一个解决兼容性问题的利器——小数 « 张鑫旭-鑫空间-鑫生活 搜狐白社会似iphone短信对话框效果的优化 « 张鑫旭-鑫空间-鑫生活 关于文字内容溢出用点点点(…)省略号表示 « 张鑫旭-鑫空间-鑫生活 jquery之append与insertBefore使用实例 « 张鑫旭-鑫空间-鑫生活 jQuery之图片关联伸缩效果 « 张鑫旭-鑫空间-鑫生活 jquery之图片左右切换动画效果 « 张鑫旭-鑫空间-鑫生活 jQuery之replace字符串替换实现不同尺寸图片切换 « 张鑫旭-鑫空间-鑫生活 jQuery-实现图片的放大镜显示效果 « 张鑫旭-鑫空间-鑫生活 jQuery-单击文字或图片内容放大显示效果插件 « 张鑫旭-鑫空间-鑫生活 reflection.js-实现图片投影倒影效果js插件 « 张鑫旭-鑫空间-鑫生活 jQuery-很酷的弹出层效果js插件 « 张鑫旭-鑫空间-鑫生活 复选框单选框与文字对齐问题的研究与解决 « 张鑫旭-鑫空间-鑫生活 css margin的相关属性,问题及应用 « 张鑫旭-鑫空间-鑫生活 IE6下png背景不透明问题的综合拓展 « 张鑫旭-鑫空间-鑫生活
边译边学-QUnit下的JavaScript自动化单元测试 « 张鑫旭-鑫空间-鑫生活
张鑫旭,zhangxinxu · 2013-04-09 · via 张鑫旭-鑫空间-鑫生活

一、得了开头不吐槽不舒服的病

我昨晚非一人去看《北京备胎西雅图》了,还不错哦~

擦,又来了,伙计。我们很忙的,技术不相干的吐槽留着对你媳妇讲吧!

邵瑞啊!

如果“扫瑞”有用,那还要警察叔叔干嘛!

(⊙o⊙)…

以前曾听过“测试驱动的JS开发”分享,至于具体内容是什么,已经忘得一干二净,不过知道了这个东西,就是不错的收获。经常看到听到“JS单元测试”,至于具体怎么操作,也是不得而知,因为实际的工作并未使用之。昨天,看到了一位前端牛人肉身翻墙找工作的经历,其中就提到,其回答Facebook的面试题时候把单元测试代码一并发过去了。

妈妈千句唠叨不及朋友一语点拨,这里的“单元测试”一下子戳中了我的小小神经。于是,今天,工作之余,开始接触”JS单元测试”相关的一些内容。学习目标为,要对“单元测试”要足够感性整体的认识,清楚如果在自己项目中使用单元测试,该怎么做,可能会遇到哪些阻碍。

目前,有不少别人说不错的JS单元测试框架,比方说……额~我去,刚那个网页被我close掉了,大家自己搜下吧。其中有一个是QUnit, 貌似是jQuery团队捣鼓的(并不依赖jQuery)。本文就是要介绍如何使用QUnit做单元测试。至于为什么选择QUnit, 那是因为据说QUnit新手最好上手。

可否讲讲单元测试的必要性呢?

这个嘛,网上的说法是:天赐一双金钛狗眼,立显臭虫得瑟嘴脸。我自己尚未深入尝试,单元测试是否有必要我并不确定,我会在文章最后回答此问题。

二、QUnit是什么来路?

QUnit是什么来路?

QUnit出生于2008年5月,走出testrunner嵌入jQuery核心存储库。从此,它有了自己的名称和文档和一个安放代码的新家。2009年末QUnit被重构,独立于jQuery,使其可用于测试各种各样的JavaScript框架和应用程序。QUnit宣称其方法现在遵循CommonJS维护规范。虽然QUnit可以运行在服务器端或命令行环境中,但它最有效的是测试浏览器中的JavaScript。

官方自然捡好话讲,说QUnit是个强大简单易用的JavaScript测试框架。jQuery, jQuery UI, jQuery Mobile都是使用Qunit做单元测试的,甚至包括它自己。我抓了抓头发日渐稀少的脑袋瓜,决定自己亲自实践,验证是否如官网上所说的强大简单易用。

三、没错,是Hello World!

接触新的语言等,跑通的第一个例子是Hello World! 毕竟是翻译,这里也俗套一把。在测试代码展示之前,我们需要链接几个静态资源,不难想到,是QUnit的JS资源以及CSS样式资源。目前版本是1.11.0. 您可以点击下面的链接下载:

也支持npm安装。如下代码:

npm install qunitjs

如果您懒得下次,直接include静态资源也是可以的,有CDN,代码如下,官方截图如下下:

<link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.11.0.css" />
<script src="http://code.jquery.com/qunit/qunit-1.11.0.js"></script>

调用地址截图

热身完毕,煮青蛙开始啦~~

如下简单基本的测试代码:

  1. <!DOCTYPE html>
  2. <link rel="stylesheet" href="qunit.css">
  3. <script src="qunit.js"></script>
  4. <script>
  5. test("hello", function() {
  6.     ok(true, "world");
  7. });
  8. </script>
  9. <h1 id="qunit-header">QUnit Hello World</h1>
  10. <h2 id="qunit-banner"></h2>
  11. <ol id="qunit-tests"></ol>

这是一个非常简约的测试案例。定义了文档类型,包含了QUnit需要的CSS以及JS,定义了另外一个script元素,script中为单个QUnit测试代码,然后页面上还有一段用来输出结果的标记(如<h2 id="qunit-banner">)。test()函数定义了一个名叫"hello"的测试。一旦页面载入完毕,QUnit就会运行test()。传给test()的第二个参数为函数,这才是测试的真身,此处为ok()方法。ok()方法接受两个参数,第一个参数用来表明测试是通过了呢还是没有,第二个参数则是需要输出的信息。上面这个例子,第一个参数为true, 因此,就算指鹿为马也是通过的,实际上没做任何实质性的测试。

如果我们在浏览器中运行上面的代码,则会如下面这模样:

pass结果示意

截图为虚,亲见为实,您可以狠狠地点击这里:true下死活passed demo

显然,如果是:

  1. test("hello"function() {
  2.     ok(false, "world");
  3. });

则永远不会通过的:

测试不通过

从截图看以看出,"word"这个错误信息输出来了(验证了ok()第二个参数的功能),同时还标注了错误代码的位置。

截图为虚,亲见为实, too,您可以狠狠地点击这里:false下死活failed demo

有点样子,下面是不是该来点真格的啦。

Bingong! 恭喜你,你都会抢答啦!

三、动真格的QUnit测试

只喝汤不吃肉,婉君也会皱眉头。因此,来个更加完整的例子哈:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4.     <title>QUnit测试</title>
  5.     <link rel="stylesheet" href="qunit.css">
  6.     <script src="qunit.js"></script>
  7.     <script src="tests.js"></script>
  8. </head>
  9. <body>
  10.     <h1 id="qunit-header">QUnit测试</h1>
  11.     <h2 id="qunit-banner"></h2>
  12.     <div id="qunit-testrunner-toolbar"></div>
  13.     <h2 id="qunit-userAgent"></h2>
  14.     <ol id="qunit-tests"></ol>
  15.     <div id="qunit-fixture">测试标记</div>
  16. </body>
  17. </html>

与之前代码不同的是,这里多了个tests.js, 以及HTML部分有了更多的标记,如<h2 id="qunit-userAgent">等。tests.js中内容为:

  1. function format(string, values) {
  2.     for (var key in values) {
  3.         string = string.replace(new RegExp("\{" + key + "}"), values[key]);
  4.     }
  5.     return string;
  6. }
  7.  
  8. test("basics"function() {
  9.     var values = {
  10.         name: "World"
  11.     };
  12.     equal( format("Hello, {name}", values), "Hello, World""单个匹配" );
  13.     equal( format("Hello, {name}, how is {name} today?", values),
  14.         "Hello, World, how is World today?""多个匹配" );
  15. });

上面代码要做的事情不难理解,format函数的功能就是模板HTML替换技术,MVC的基础之一。

这里使用的是equal()进行测试,equal()方法作用是做比对,如何比对法呢?很简单,使用JavaScript的"=="操作符比对其前两个参数。其中第一个参数为实际的值,本例子就是format()的返回值;第二个参数为我们期望得到的值,格式化的字符串。

OK, 结果到底如何呢?我们先看截图一览究竟:

多匹配不通过

哇塞,好详细的错误信息提示啊,详细到应该什么结果实际什么结果,甚至还有错误代码位置,不错不错!

恩,确实,说实话,我开始有点喜欢QUnit了。上例子有demo的,勿浪费,您可以狠狠地点击这里:更完整多匹配测试不通过demo

发现臭虫,事情就好办多了,多匹配出错,很快就会发现原来是正则表达式那里,全局匹配的"g"给弄丢了,拿起敌敌畏喷上就噢啦。

  1. new RegExp("\{" + key + "}""g")

于是乎,两个测试都可以通过啦。如果是实际项目,显然,这个测试有必要做进一步的扩展,匹配的对象不止一个关键字以及对象关键字要缺失(是否显示undefined)等。接下来,我们要讲讲如何避免一个测试搞跛另外的测试。coffee time~

一休哥

四、颗粒、单元

如果仅仅是毫无副作用的功能测试,那实际上很轻松的。但是,一旦代码具有副作用,例如修改DOM或者改变全局变量,事件就变得棘手了。因此,有必要让测试颗粒相互独立,降低可能的副作用。QUnit提供的一些工具以便使用。

特定的DOM

如果要测试DOM修改,我们可以使用#qunit-fixture元素。#qunit-fixture元素好比游戏中的小怪,你可以随便砍杀增长经验,而不要担心第二天上机的时候小怪都没了。在做测试的时候,这个静态标记你可以随便置空或append元素,每个测试结束的时候QUnit会自动把#qunit-fixture元素的innerHTML属性值设置为初始值。如果可以使用jQuery,QUnit会使用jQuery的html()方法代替,同时清除jQuery事件句柄。

举个例子,我们想测试HTML5 placeholder的填充性,我们会有一个input元素并带有placeholder属性:

  1. <div id="qunit-fixture">
  2.     <input id="input" type="text" placeholder="placeholder text" />
  3. </div>

现在,每个测试你都可以选择#input元素,打他,骂他,QUnit之后都会让它满血复活的。

module – 模块

如果我们需要更多清除,而不仅仅是重新设置DOM,我们可以作为每个测试的一部分实现。如果在多个测试中,我们最终做的是同一个清除,我们可以使用module()方法重构。

不太懂~~

上面是字面翻译。通俗讲就是:如果你有很多的推倒、爆菊之类,你可以每次测试搞一下。如果要搞很多次,每次都是爆菊,则你可以使用module()方法重构。

亮了!

module()方法初衷是分组测试,例如,多个测试都测试一个特殊的方法,该方法就可以作为单个模块的一部分。这有助于过滤测试(见下面“QUnit下的开发”)。为了让测试更加颗粒化,我们可以使用module()提供的setupteardown回调。

  1. module("core"{
  2.     setup: function() {
  3.         
  4.     },
  5.     teardown: function() {
  6.         
  7.     }
  8. });
  9. test("basics"function() {
  10.     
  11. });

setup回调都是在测试之前运行,teardown回调是在测试之后运行。

我们可以使用这些回调创建对象并在测试中使用,而无需依靠闭包(或全局变量)传给测试。之所以会起作用,是因为setupteardown以及真实的测试都在一个自定义的可以自动共享和清除的作用域中调用。

似懂非懂,举个例子呗。

正有此意。下面的代码是个简单(但不完整)的库,用来处理货币值。

  1. var Money = function(options) {
  2.     this.amount = options.amount || 0;
  3.     this.template = options.template || "{symbol}{amount}";
  4.     this.symbol = options.symbol || "$";
  5. };
  6. Money.prototype = {
  7.     add: function(toAdd) {
  8.         this.amount += toAdd;
  9.     },
  10.     toString: function() {
  11.         return this.template
  12.             .replace("{symbol}"this.symbol)
  13.             .replace("{amount}"this.amount)
  14.     }
  15. };
  16. Money.euro = function(amount) {
  17.     return new Money({
  18.         amount: amount,
  19.         template: "{amount} {symbol}",
  20.         symbol: "EUR"
  21.     });
  22. };

上面代码创建了个Money对象,有个为美元(dollars)创建的个默认构造器方法,还有一个为欧元(euro)创建的工厂方法。这两个方法作用都是操控和打印。这里,我们不是为每个测试都创建一个新的Money对象,而是使用setup回调创建对象并存储在测试作用域中。

  1. module("Money"{
  2.     setup: function() {
  3.         this.dollar = new Money({
  4.             amount: 15.5
  5.         });
  6.         this.euro = Money.euro(14.5);
  7.     },
  8.     teardown: function() {
  9.         
  10.     }
  11. });
  12.  
  13. test("add"function() {
  14.     equal( this.dollar.amount, 15.5 );
  15.     this.dollar.add(16.1)
  16.     equal( this.dollar.amount, 31.6 );
  17. });
  18. test("toString"function() {
  19.     equal( this.dollar.toString(), "$15.5" );
  20.     equal( this.euro.toString(), "14.5 EUR" );
  21. });

上代码中,setup回调中创建了两个对象,名叫"add"的测试之使用了一个,"toString"这个测试两个对象都使用了。本例中,teardown回调没必要使用,因为没必要要移除已经创建的Money对象。结果如下截图:

module使用测试结果截图

您可以狠狠地点击这里:module与单元测试demo

五、测试异步代码

我们已经看以,QUnit可以控制什么时候真正运行测试代码,只要代码是同步运行的。然而,一旦测试的代码需要使用异步回调(如setTimeout或Ajax请求),你需要给QUnit反馈,以便停止后面的测试,知道你让它再次运行。

使用的方法很简单,就是start()end(),如下代码:

  1. test("async"function() {
  2.     stop();
  3.     $.getJSON("resource"function(result) {
  4.         deepEqual(result, {
  5.             status: "ok"
  6.         });
  7.         start();
  8.     });
  9. });

这里jQuery的$.getJSON方法被用来从"resource"中请求数据,然后判断结果。deepEqual(有别于开始的equal)用来检测结果是否完全是我们所期望的(比方说var a={}, b ={}; // a==b → false,但是,使用deepEquala==btrue)。

因为$.getJSON是异步的,我们调用stop(), 然后运行代码,再在callback结束的时候调用start(), 告诉QUnit继续运行。

运行异步代码的时候,如果没有使用stop(),会导致本意是其他测试的任意结果,如passedfailed.

asyncTest

如果我们使用asyncTest()代替test(), 我们就可以把stop()省掉。于是,我们一眼就可以看出这是个异步测试。

  1. asyncTest("async2"function() {
  2.     $.getJSON("resource"function(result) {
  3.         deepEqual(result, {
  4.             status: "ok"
  5.         });
  6.         start();
  7.     });
  8. });

继续,还有其他的API方法。

expect

测试回调的时候,无论是不是异步,我们都不能确定回调会在某些时候被真正调用了。为了应付这种状况,我们可以使用expect(), 定义一个测试中我们期望的判断个数。这样就可以避免本应出现两个判别结果的,结果一个通过后就停止的情况。

  1. asyncTest("async3"function() {
  2.     expect(1);
  3.     $.getJSON("resource"function(result) {
  4.         deepEqual(result, {
  5.             status: "ok"
  6.         });
  7.         start();
  8.     });
  9. });

这里,asyncTest()中,expect被调用,参数为1, 相对于告诉QUnit这里的测试应该只有一次判定。这有什么好处呢?当我们同时做多个异步测试的时候,可能其他的测试嗝屁被破坏,然后也弄出了一个判定,此时expect就可以捕获这个问题。

expect()貌似有个取巧的设置方法,就是给test()asyncTest()的第二个参数传递一个数字,例如,上面的代码可以写成:

  1. asyncTest("async4"1function() {
  2.     $.getJSON("resource"function(result) {
  3.         deepEqual(result, {
  4.             status: "ok"
  5.         });
  6.         start();
  7.     });
  8. });

是不是应该放demo啦。

稍安勿躁,还有一部分,介绍完了一起放出来。

异步信号

如果测试的结束点多个-多个回调的顺序随机-我们可以使用QUnit内置信号。调用stop()与之后调用的start()同数目,则Qunit会继续运行直到stop()增加的计数被start()减少会0.

  1. test("async semaphore"function() {
  2.     stop();
  3.     stop();
  4.     $.getJSON("resource"function(result) {
  5.         equal(result.status, "ok");
  6.         start();
  7.     });
  8.     $.getJSON("resource"function(result) {
  9.         equal(result.status, "ok");
  10.         start();
  11.     });
  12. });

说好的demo呢?

真**猴急,您可以狠狠地点击这里:上面所有异步测试集合demo 源代码右键可查看。

异步测试结果截图 张鑫旭-鑫空间-鑫生活

六、更多的测试判断

equaldeepEqual已经在前面登台亮相了。实际上,QUnit提供了远不止这几个判断,比方说strict, 使用和eauql一样,除了使用的是JavaScript中的"==="严格对比操作符。当对比不同类型值的时候,例如数值和字符串比较,eauql就有用武之地了,这样你的测试就不会死在0等于"0"上

有正必有邪,因此,跟equal, deepEqualstrictEqual相反,这里还有notEqual, notDeepEqual, notStrictEqual. 同样的规则,不一样的结果。

raises

除了这些,还有raises(类似CommonJS中的throws,不过没有使用保留关键字). 你传递一个function作为第一个参数,其会被QUnit中的try-catch语句中调用。如果这个函数抛出异常,则我们认为通过-pass,否则不通过-fail。你还可以使用第二个参数检测抛出的异常。可以是正则表达式,或者是构造函数(使抛出错误会instanceof这个构造函数)检测,或者是一个返回truefalse的函数(此函数的第一个参数就是抛出的异常)。

  1. test("raises"function() {
  2.     function CustomError() {}
  3.     raises(function() {
  4.         throw new CustomError();
  5.     }, CustomError, "must throw error to pass");
  6. });

上面示例显示了raises如何通过传递预期的构造函数引用作为第二个参数来检测构造函数抛出的异常。结果如下图,通过:

异常检测结果 张鑫旭-鑫空间-鑫生活

之所以会有上面的结果,是因为抛出的错误 instanceof CustomErrortrue. 您可以狠狠地点击这里:抛出异常检测demo

七、自定义判断

就是让我们直接使用QUnit.push()进行封装自定义一些判断方法, 而不是上面的equal之类。QUnit.push()equal等关系,就好比jQuery中$.ajax()方法和$.post()/$.get()之间的关系。下面例子就是使用QUnit.push()自定义的一个验证DOM的某些属性、包括子元素前后是否发生变化的方法。

  1. function domEqual( selector, modifier, message ) {
  2.     var attributes = ["class""role""id""tabIndex""aria-activedescendant"];
  3.     
  4.     function extract(value) {
  5.         var result = {};
  6.         result.nodeName = value[0].nodeName;
  7.         $.each(attributes, function(index, attr) {
  8.             result[attr] = value.attr(attr);
  9.         });
  10.         result.children = [];
  11.         var children = value.children();
  12.         if (children.length) {
  13.             children.each(function() {
  14.                 result.children.push(extract($(this)));
  15.             });
  16.         } else {
  17.             result.text = value.text();
  18.         }
  19.         return result;
  20.     }
  21.     var expected = extract($(selector));
  22.     modifier($(selector));
  23.     
  24.     var actual = extract($(selector));
  25.     QUnit.push( QUnit.equiv(actual, expected), actual, expected, message );
  26. }

上面代码虽然蛮多,其实要做的事情不难理解。检测选择器selector对应的元素attributes中的属性以及子元素是否在modifier()这个方法执行之后发生了改变。下面就是测试代码(需要用到jQuery UI):

  1. test("destroy"function() {
  2.     domEqual("#autocomplete"function() {
  3.         $("#autocomplete").autocomplete().autocomplete("destroy");
  4.     });
  5. })

在页面上放了个HTML为<input id="autocomplete">的代码,结果如下图:

autocomplete下自定义domEqual方法的测试 张鑫旭-鑫空间-鑫生活

貌似modifier执行后,Autocomplete插件将文本框的className改成了空字符串,导致文本框前后变化不相等了,于是检测结果为failed!如果默认HTML是<input id="autocomplete" class="">,则结果是passed!您可以狠狠地点击这里:autocomplete下自定义domEqual方法检测demo

八、使用QUnit做开发

我勒个去,怎么还有这么多内容,我要偷工减料。

您可能注意到结果输出面板上有个不太明显的Rerun按钮,意思是“重新运行”,页面本来就可以刷新,还需要Rerun做什么呢?在实际测试的时候,测试面板肯定有很多结果的,有时候,我们可能会对某个测试结果很关心(比方说测试未通过),我们就可以点击Rerun按钮,于是,页面就只会检测该部分的代码是否可以通过了。例如,我点击本文第5个demo的第4个Rerun按钮,则:

点击rerun按钮

点击rerun按钮后

通过格式化的url地址筛选需要运行的检测代码,这里,就指检测了第4个Rerun按钮对应的代码。

调试工具

第3个demo中,您可能注意到了这个:

调试工具

这些就是QUnit的调试工具,至于干嘛用的,偷工减料,不解释,(*^__^*) 嘻嘻……

伙计,这可是煤油灯,不能省的,再辛苦下呗~~

好吧,内心的另一个我在召唤,加油!

  • Hide passed tests
    “隐藏通过的测试”,很好理解,勾选后,通过的测试就不显示了,如下第3demo截图:
    隐藏通过的测试截图
  • Check for Globals
    “全局检测”,如果勾选此项,在运行测试之前,QUnit会枚举window所有属性,然后与运行结束之后的window做比较,如果前后不一样,哦呵呵,就会显示不通过-failed, 以及显示“引入或缺失全局属性”。该工具作用在于可以很容易被发现判断导致全局变量的引入.
  • No try-catch
    “不要try-catch”,选中则意味着QUnit会在try-catch语句外运行回调,此时,如果测试出现异常,测试就会停止。这有什么用呢?要知道,有些浏览器的调试工具是相当弱的,尤其IE6,一个未处理的异常要比捕获的异常可以提供更多的信息。即使再次抛出,由于JavaScript不擅长异常处理,原来的堆栈跟踪在大多数浏览器里都丢失了。

    这显然是一个有着特殊目的的工具。然而,一旦你遇到一个异常,同时无法追溯真正错误代码的时候,这个工具就显得很有用了。

九、更高效的测试驱动开发

对于测试驱动开发(TDD – test-driven development)而言,QUnit是相当不错的插件。然而,如果我们稍作调整,Qunit会变得更加有用,无论是结果还是速度。

假设现在有一个巨大的测试套件,全部测试完毕需要10秒钟,然后某处失败了,改好后,你重新测试,又是10秒钟,结果又发现一个错误,可能就是30秒甚至1分钟,所谓测试驱动开发难免沦为鸡肋。虽然Rurun过滤可以有针对性测试,但是,双拳难敌四手,错误很多的话根本应付不来。

那该怎么办呢?

这种情形,我们可以借助调试工具中的Hide passed tests这个选项提高开发效率。

看上面的介绍,这个工具很一般啊,这里为什么变超人了呢?

其实Hide passed tests可不是隐藏显示那么简单,其使用了HTML5 sessionStorage技术,在不算离谱的浏览器下,会记住之前未通过的测试,然后页面重新载入的时候只测试之前那部分为通过的代码。显然,就不会出现上面那种改一下10秒跑一遍,改一下10秒钟跑一遍的情况。

十、回答开始的问题、结语

QUnit相关知识整理下来,我就可以很肯定地回答一开始抛出的问题:单元测试的必要性?答案是:有必要
实际上,虽然我之前没有接触过单元测试相关的知识,但是,自己在实际写组件、方法的时候都是做过单元测试的,但是,属于一种业余的做法。比较好的单元测试应该是以代码去验证代码,测试代码保留,以便后续可以继续使用;而自己,纯粹是伪造各种可能的数据,然后去跑代码,是否报错之类,然后跑通之后,之间的测试数据什么的都删掉了,只剩下真正需要的运行代码。

实际上这是有问题的:

  1. 如果这个方法后期内部逻辑有大改,回头测试的时候,自己岂不是要重新再造一遍数据,哦,我宝贵的时间啊~~;
  2. 如果这个方法后期内部逻辑小改,实际测试的时候,精力成本等原因,往往就只会测试修改的这部分。很可能就会导致回归错误(A处改动触发B处的旧bug)。如果保留有完整的单元测试代码,因为想省事而出现的回归错误显然就无所遁形了!

可见,就个人而言,单元测试的思想是有的,但是,单元测试的做法有问题。借助类似Qunit这样的工具是很好的选择。因此,接下来,我会在团队中推广单元测试。磨刀不误砍柴工,大家权衡成本收益等,可以在关键方法(如果刚接触,没必要所有的方法)上试试单元测试,获取,你会喜欢上它的。

走遍万水千山,终于……看到了一家渔具店,故事结束了。由于路上走得匆忙,沿途可能踩到了花花草草,欢迎指正,欢迎交流。感谢您一直阅读到此处!