w3ctech 2011 JavaScript 专题会议(上海站)最近在张江畅星大厦召开,参会者 200 多人,来自国内技术社区的四位知名专家高博、权一、杜欢、贺师俊分别做了精彩的演讲,涉及的内容包括测试驱动开发、ES5 新特性、iOS 上的 Web 应用、Javascript 框架 API 设计思想等。InfoQ 中文站整理了大会的精彩内容,供读者参考。
测试驱动的 JavaScript 开发入门
来自盛大创新院的高博以自己翻译的书“测试驱动的 Javascript 开发”为背景,结合在测试工作中的实践经验,介绍了测试驱动在 Javascript 开发中的应用。
什么是测试驱动开发?高博指出,在传统的开发过程中,大家遵循着先开发后测试的阶段模式,按部就班地需求分析、设计、开发、测试、发布。而在测试驱动开发过程中,测试环节位于开发环节之前,甚至取代了设计环节。测试用例代码完成在先,实际产品代码完成在后,这意味着第一行代码是测试用例代码。测试的结果是开发进度的指挥棒,测试推进到哪里,开发才能跟进到哪里。测试的结果是“失败“,才能继续进行开发工作,直至测试的结果成为”通过“为止。
高博举了一个有关 Javascript 的测试驱动例子,需求是”开发一个 JavaScript 字符串函数,实现去除字符串开始和结尾的空白字符的功能”。如何编写第一个测试用例?他认为首先要对需求分解,在这个例子中,可以把”开头“和”结尾“当成两个小的需求,另外不能忽略隐含的需求,比如假定被测试的函数名为 trim,那么第一个测试用例可能是判断其在作用域中是一个函数:typeof “”.trim == “function”。回到刚才分解的两个需求上来,针对每一个需求写一个测试用来,第一个看起来像这样:
"test trim should remove leading white-space": function () { assert("should remove leading white-space", "a string" === " a string".trim()); }
即测试去掉开头的空白字符。接下来,运行该测试用例,因为没有还没有定义 trim,所以测试结果是失败的,不过在测试驱动的开发中,测试用例的失败结果才是开发能够跟进的唯一动力。可是,为什么明知道测试会得到失败结果,还是要运行一次测试用例?高博指出,这样做的一个原因是测试在当前作用域下存在函数冲突。
接下来实现第一个小需求:
String.prototype.trim = function () { return this.replace(/^\s+/, ""); };
高博特别强调,在开发产品代码时,要暂时忘掉原始的需求,现在的唯一目的是让当前测试用例的结果从失败变成通过。在通过了第一个小需求之后,编写第二个测试用例,并修改产品代码使其通过第二个测试。在每一步撰写了更多产品代码后,高博提醒开发人员要做两件事:
- 把所有之前的测试用例都要运行一次,并且要全部得到通过结果。这样做可以发现由于代码更改而引入的新问题。
- 随时对代码进行重构,以保证用更好的等价代码实现同样的目的。
最终的函数定义为:
String.prototype.trim = function() { return this.replace(/(^\s+)|(\s+$)/, ""); };
在测试驱动开发中,存在着一个循环,即”撰写引起新的失败结果的测试用例“——>”撰写新的产品代码以使之前的测试用例通过“——>”重构以得到更好的等价产品代码“——>”撰写引起新的失败结果的测试用例”——>…在这里,测试用例是循环的第一推动力,测试用例在每轮循环的起始代表着需求分析的分解结果,开发过程表现为满足测试用例的活动,测试用例在每轮循环的结束代表着开发规格。高博总结说,测试驱动将难以估算的抽象需求转换为一系列明确的、无二义性的规格。由测试驱动的开发过程是增量式的,由测试驱动的开发无须等到全部的需求明确即可开始,遵循不做无用功原则。测试驱动的本质是将测试用例分别作为需求和规格加入开发过程首末两端的持续集成技术。
为什么要将测试驱动的开发方法引入 JavaScript 开发呢?高博分析了 Javascript 语言的特点:极其灵活,书写风格几乎无穷无尽;操作对象复杂,需要面对各种物理和逻辑对象;可以和各种动态语言混合,作为它们的就地(in-place)输入或输出;虽然有标准可循(ECMA-262),但是各种环境对标准支持的程序区别很大;解释型语言,调试的工具和手段比较缺乏。由此导致开发人员容易防御性地过度设计,在开发过程中对产品代码的信心不足。而这些不足恰好是测试驱动开发的用武之地:科学地设计测试用例及其自动化,逐步覆盖需求;每通过一个或一组测试,开发者信心就增强一分。
对于单元测试,高博建议:积累测试用例集,形成回归测试的规模和覆盖率;欲修改产品代码,首先更新测试用例;测试用例彼此独立,避免耦合;只测试 JavaScript,不测试与外部依赖项目的耦合部分(数据库、文件系统、用户界面等);测试用例的建立要多快好省;产品代码要避免形成测试障碍和阻塞(如弹出对话框、Ajax 异步调用等)。
对于性能测试,高博认为通过 Javascript 性能测试,开发人员能够在问题的多种解决方案中,比较优劣。性能测试结果使 JavaScript 的开发避免了盲目“优化”性能,以及无限制地“改进”性能。
对于当前火热的 Node.js 技术,高博介绍了 NodeUnit 工具,并提供了测试示例。除此之外,他还介绍了 Stub 和 Mock,Stub 能够像真实的被测试环境一样提供根据输入给出对应输出的模拟对象。通常实现比真实对象要简单,但是从给定的测试用例集合的输出结果看不出它与真实对象的区别。Mock 是一种输出结果预知的测试对象。Stub 仍然是先执行测试,再得到输出(该结果可能是受控的),而 Mock 则是直接给出测试输出。Mock 对象常用于测试的输出结果不常见的场景,以求得更高的测试覆盖率。
ECMAScript Edition5 尝试
来自盛大在线的 Web 前端工程师权一为大家讲解了 ECMAScript 5 引入的重要新特性,虽然该版本早在 2009 年 12 月份已经发布,但是由于浏览器的兼容性问题,一直让大家对 ECMAScript 5 敬而远之。随着浏览器版本的更新,特别是最近 Node.js 的兴起(其依赖的 V8 引擎支持 ECMAScript 5),开发人员开始尝试应用于实践中。权一目前正在对 ECMAScript 5 规范进行中文注解,所以他的讲座内容很全面。
目前,ECMAScript 5 的支持情况较好的浏览器包括 Firefox4+、Chrome11、IE10PP2,还有后端 JS 运行时 NodeJS(V8)。
权一首先分析了 ECMAScript 5 引入的新 API,包括:
- Object.create(prototype, descriptors)——创建一个对象,指定其 prototype,而且可以包含若干描述符对象。
- Object.defineProperty(object, propertyname, descriptor)——为对象增加属性(property),或者修改现有属性的配置(attribute)。
- Object.freeze(object)——禁止现有对象属性的配置和值的更改,并禁止增加新属性。
- Object.seal(object)——禁止现有对象属性的配置的更新,并禁止增加新属性。
- Object.keys(object)——返回对象中可枚举的属性和方法。
- Object.preventExtensions(object)——禁止增加对象的属性。
在这些 API 中,经常会遇到一个概念,即属性描述符。它是用于解释某一个被命名的属性具体操作规则的特性集。 属性描述符中的对应每个字段名都会有一个值,其中任何一个字段都可以缺省或显式的设置。 属性描述符还会被进一步以字段的实际用途来分类成数据属性描述符访问器属性描述符。比如,在 Object.create 和 Object.defineProperty 方法的参数中会涉及到属性描述符。
数据属性可以可以存储和获取值,其描述符包括:
- value——属性的当前值,缺省为 undefined。
- writable——true 或者 false。如果为 true,属性值可以被修改。缺省为 false。
- enumerable——true 或者 false。如果为 true,属性可以在 for…in 语句中被枚举。缺省为 false。
- configurable——true 或者 false。如果为 true,属性描述符可以被修改,属性可以被删除。缺省为 false。
数据属性的代码示例如下:
var obj = {}; Object.defineProperty(obj, "newDataProperty", { value: 101, writable: true, enumerable: true, configurable: true });
访问器属性(accessor)在每次存储或者获取属性值时都会调用用户提供的函数,描述符包括:
- get——返回属性值的函数,无参数,缺省为 undefined。
- set——函数包含一个参数,即传递的值,该函数用于设置属性值,缺省为 undefined。
- enumerable 和 configurable 的设置与数据属性描述符类似。
访问器属性的示例代码如下:
var obj = {}; Object.defineProperty(obj, "newAccessorProperty", { set: function (x) { document.write("in property set accessor" + newLine); this.newaccpropvalue = x; }, get: function () { document.write("in property get accessor" + newLine); return this.newaccpropvalue; }, enumerable: true, configurable: true });
权一指出,新增加的 Object.create、defineProperty、freeze、seal、preventExtensions 等方法会让开发人员在使用 Javascript 进行面向对象编程时更加方便。
ECMAScript 5 还引入了严格模式(use strict),能够更好地执行错误检查。use strict 可以声明一个文件、程序或者函数,这种声明方式称之为指令序言,它的作用域可以是整个程序,也可以是某个函数体内。
严格模式限制的内容主要包括:
- 使用未声明的变量。
- 对只读属性进行赋值操作。
- 对不可扩展对象增加属性。
- 在对象中重复定义同一个属性。
- 使用预留的关键字作为变量名或者函数名。
- 使用 with 语句。
Build Web Apps for iOS
Cisco CSG 前端架构师杜欢的演讲题目是《Build Web Apps for iOS》,不过从其讲座内容来看,是以 iOS 为例,讲述了 Web 应用在 CSS3、HTML5、UI 设计思想中的经验。
对于设计原则,杜欢强调了以下几点:
- 保持干净整洁的风格
- 保持 UI 元素的一致性
- 谨慎处理渐进增强
在实现设计风格时,我们需要重点考虑的地方是这个应用的目的是什么,所有的设计是否为此而存在,所有呈现给用户的功能是否是用户期望的,不走“混搭风”。保持 UI 的一致性能够降低用户的学习成本、增强应用的整体性。在渐进增强处理时,需要考虑设备的输入输出和特性支持,以及不同浏览器的差异。
杜欢以 iOS 设备为例,使用条件式 CSS 来指定特定样式:
<link media="only screen and (max-device-width: 480px)" href="small-device.css" type= "text/css" rel="stylesheet">
对于页面的视窗(Viewport)设定,可以参照设备的显示宽度:
<meta name ="viewport" content = "width = device-width">
杜欢还提到了在 wifi 不工作的离线状态下,如何开发离线应用,即利用缓存清单 MANIFEST:
MIME type = text/cache-manifest <html manifest=“resource.manifest”>
如果需要存储离线数据,则可以通过离线存储,如本地存储 WEB Storage、WEB SQL、IndexedDB。
在演讲最后,杜欢总结了 iOS 开发中需要注意的一些限制:
- 解码后的 GIF, PNG, TIFF 图片尺寸不超过 3M(width * height ≤ 3 * 1024 * 1024)。
- 解码后的 JPG 图片最大尺寸不超过 32M 像素(使用二次抽样)。
- Canvas 元素的最大尺寸不超过 3M 像素。
- Canvas 元素的默认宽高为:150 x 300。
- 单个 HTML, CSS, JS 或非流媒体格式的资源文件必须小于 10M。
- 任意一段 JavaScript 执行时间不超过 10 秒。
- 一个 page 要 load 的所有资源总大小保持在 30M 以内。
- 请使用 ECMAScript 3。
关于 JavaScript 库和框架的 API 设计的思考两三例
资深 Web 开发专家贺师俊在讲座中对 API 的设计和评判标准做了深入的探讨。他首先借用了《软件框架设计的艺术》中的“无绪”(Cluelessness)概念,软件工程中的无绪是指:程序员无需深入了解很多内容,也可写出好的代码找到一种编程实践方法,让开发人员不用深入了解所有事情,即选择他们所需的知识。API 是对所需知识的抽象,它将系统的复杂性隐藏起来。
评价 API 好坏的标准包括:
- 可理解性
- 一致性
- 可见性
- 简单性
- 保护投资(兼容性)
贺师俊以 jQuery 为例,其中的 Selector 体现了可理解性,$ 体现了可见性,而相比 Dojo、YUI,jQuery 在兼容性方面表现出色。接着,他又举了 Pyhon 框架、Grails、Prototype.js、短命名等例子来分析 API 的设计问题,让读者有了更深的理解。他用较长的篇幅分析了 Lazy function 模式在 API 设计方面的缺陷,并强调不能以防御式编程和提供文档为借口来降低 API 的质量。
最后,贺师俊总结 API 设计的一些经验:
- API 的目的是让开发者可以选择性无绪。
- 评价 API 的好坏就是从“选择性无绪”出发,以此衍生出可理解性、一致性、可见性、简单性、保护投资(兼容性)等标准。
- 即使是看似主观的风格问题,也是可能按照标准结合情境进行分析的。
- 注意 JavaScript 的一些特点导致的特殊问题,如 prototype,如动态语言特征,如 ES5、Harmony 新特性带来的 API 设计的进化等。
没有参会的朋友可以通过主办方 w3ctech 的相关页面下载幻灯片和观看演讲视频。InfoQ 中文站将继续关注国内 Web 技术社区的发展。
评论