讨论: TDD in HTML & JavaScript 之可行性和最佳实践

题外话

昨天就想发起这个话题的讨论,只是觉得对于讨论的支持,博客园现有的功能天然似乎还不能很好的支持。所以有了突然发现想在博客园发起一个有价值的讨论其实很难一文。亚历山大同志提到“博客园的讨论需要发起争议性话题,比如 .net sucks之类”。回顾如关于近期C#大论战的回应这样的近期引起讨论的焦点话题,貌似确实如此。深以为叹。近期的C#大论战是幸运的,尽管中间还是参杂了很多口水,李建忠老师的加入,一定程度上最终将话题引向了正确的方向。幸哉。我这个围观群众也从中获益良多。不仅仅是对于这个技术话题正确理解,还包括李老师在他的博客最后提到的:抛掉“非要辩个胜负,分个高低”的怪诞氛围,而是来一些扎扎实实的技术说理过程,相信会更有意义——如是,则国内技术社区成长可待。

言归正传,还是尽快开始我想讨论的主题,且不管讨论最终成效如何,既然发起讨论,还是先尽可能分享自己的想法,以示诚意。

TDD的背景

自从03年Beck正式提出(事实上在00年,Beck提出eXtreme Programming时,就已经提出了这个词)Test-driven design/development这样一个基于测试优先、重构和迭代的革命性的开发方法以来,无数的实践已经证明,对于适合进行TDD的领域,TDD能够极大地提高代码的可维护性和开发效率。

TDD的基本流程图如下:

http://upload.wikimedia.org/wikipedia/en/9/9c/Test-driven_development.PNG

在这样一个迭代的流程中,在写任何的production code之前,先写test,再写production code,并且不断地对代码进行清理和重构,并且每次迭代都要进行回归测试,保证新增的test和production code不会break任何已有的test和production代码。

一般来讲,支持自动化的回归测试的工具相对比较容易实现。整个流程中的难点在于:当先行写test代码的时候,必然要求先定义被测试的production code的外部接口,对于第一次迭代,自然没有问题;但是,由于需求的变更,或者整体设计的变更,在后续的迭代过程中,经常会发生,已有的已经实现并且包含完整测试的production code的外部接口需要变更或者说重构;尽管从理论上,绝大多数的重构需求,都有规律甚至是模式可循,但是,如果完全依赖于人工操作,则不仅效率不高,且极易出错。所以,但凡成功的TDD实践,其中都不乏很多支持重构的工具。比如,现行绝大多数的集成开发环境,都有很多自动化的代码重构工具,大大的降低了代码重构的成本。

但是还有一些领域,TDD还略微有些力不从心,或者说,至少,至今没有看到太多比较好的实践案例。比如:对于Database和UI。

对于数据库开发的TDD,到目前为止面临的主要挑战是工具的支持。无论是自动化的回归测试工具,还是重构工具都还远远不够成熟。

而对于UI的TDD,则是本文的主题。

TDD in HTML & JavaScript 概述

谈到应用程序的UI,其实包括两个方面的内容:一方面是纯图形的look & feel;另一方面,则是用户和应用程序的交互。用户和应用程序的交互往往同时导致图形界面的变化,并且,转换到新的交互行为。

由于工作实践中主要是基于WEB的HTML和JavaScript的项目,这里对TDD in UI的讨论,将focus在基于HTML和JavaScript的UI。

同时,一般来讲,WEB程序的表现层主要有客户端代码和服务端代码,而服务端代码,相对来说,更容易被测试。所以,本文讨论的重点,主要focus在客户端代码。换句话说,这里讨论的TDD in HTML & JavaScript指的是对于客户端的HTML和JavaScript的TDD。

TDD in HTML & JavaScript 之可行性

说到可行性,其实可以分两个层面:理论上的可行性,和实际应用的可行性。

第一个问题是:纯图形的look & feel理论上可以进行自动化的测试吗?答案几乎是否定的。因此,主要用于呈现纯图形的HTML及CSS,也几乎是很难自动化测试的。

那么,用户和应用程序的交互理论上是否可以进行自动化测试呢?答案毫无疑问是肯定的。

WEB交互的测试其实可以根据WEB程序的架构,分为两种类型:

  1. 传统的WEB程序主要基于服务端来呈现内容,用户和页面的交互,主要是get,post数据和页面跳转。因此,对应的测试方式,主要也是由测试工具模拟需要get或post的数据,并且跟踪期望的页面跳转情况。这种情况下的测试其实相对简单,因此本文不想过多讨论。
  2. 当前的基于AJAX的WEB程序则很大程度上丰富了用户和页面交互的方式,用户和页面的交互,除了传统的get,post数据和页面跳转,在页面不刷新的情况下,还通过触发各种DOM事件,甚至直接触发JavaScript方法的执行,由JavaScript来改变和呈现内容。此时,传统的只能模拟需要get或post的数据的测试工具就无能为力了。此时由于所有的逻辑代码都在JavaScript中,所以,本质上其实是需要对大量的JavaScript代码进行测试。此正是本文希望讨论的重点。

首先,针对JavaScript的自动化测试工具其实已经有不少了,如:

Mock工具也有:

支持直接重构JavaScript代码的工具相对比较少,提供的功能也都还非常弱:

从支持工具的现状,可以说,影响TDD in JavaScript的实际可行性的因素之一是重构工具的缺乏。

不过,最近的情况有了一些改变,现在也出现了一些支持JavaScript重构的变通的解决方案,如:

  • Script# - Write C# code,compile C# source code directly to JavaScript code
  • jsc – Write any .NET code, convert .NET assembly to JavaScript, ActionScript, java or PHP code

这些方案的特点是,利用现有的IDE对流行的编程语言如C#源代码的完善的coding,尤其是强类型,重构和测试的支持,让开发人员写C#,由工具转换为可直接执行的,格式化的JavaScript代码。除了充分利用IDE对流行语言的coding支持之外,这类方案的另一个好处是,相对于高薪聘请Senior的JavaScript开发人员,Junior的C#的开发人员要便宜得多,也易招得多,但得益于Script#,已经足够能用他们熟悉的C#,写出逻辑复杂和OO的JavaScript代码,因此,开发成本被大大降低。

综上所述,TDD in JavaScript不仅理论上是可行,实际应用上,也是有足够的工具支持的。尤其是如Script#这样的工具的出现,极大地提高了JavaScript代码的开发效率。

TDD in JavaScript 之最佳实践

谁都希望能有最佳实践。什么是最佳实践呢?有很多人见不得“best”,“最”这样的词,认为,这个世界上没有“最”的东西。有吗?当然有!我们首先要略为上升到哲学的高度,对于包含“最”这样的词汇的命题,如果想要为“真命题”,则必然是需要加上一个适当的前提条件的。

比如说:我说“我是这世界上最NB的人”。这毫无疑问是个假命题。因为,缺乏适当的前提条件。你可以自己做个练习,如果觉得这个命题假,想办法给它加上更多的前提条件,一定能让它变真。

所以,所谓最佳实践,指的是,对一个或者一类特定的问题,在一个相对确定的背景下,所能采取的实际处理的方案典范。加上前提条件,则“最佳实践”当然是存在的,也是值得讨论的。

通过前面的章节,我们已经把本文重点讨论的主题,限制到一个相对小的范围,那就是对基于AJAX的WEB应用程序中的大量的JavaScript代码,如何进行TDD?

并且,我们也收集了足够的支持TDD需要的各种工具,包括自动化测试工具,Mock工具和重构工具。在这些工具的支持下,很大程度上,WEB程序客户端JavaScript代码的TDD和服务端代码的TDD,不应该有很大的区别。但同时,由于客户端代码的特殊性,自然也应该有一些客户端脚本代码所特有的实践模式。

以下首先列出本人推荐的一些实践模式,希望大家能一起修正和补缺。

最佳实践一:应用MVC模式

在传统的非AJAX的WEB程序中,JavaScript往往处于非常辅助性的地位。除了实现一些特效和数据验证等辅助功能之外,一个页面的JavaScript代码,恐怕屈指可数,自然无所谓测试,甚至是TDD了。

但是在现在的复杂的AJAX应用中,以往必须由多个独立页面的get,post和页面跳转才能组合实现的功能,通过JavaScript,可以在一个无需刷新浏览器的页面中,轻易实现,不但用户体验更佳,速度更快,对服务器的负担也更小。

此时,原本传统WEB程序的服务端需要处理的问题,如数据绑定,事件绑定,逻辑控制等,需要在客户端进行处理。也因此,原本为了解决WEB程序服务端代码可测试性问题MVC模式,也就一样可以良好的应用于客户端。清晰的将JavaScript代码分割成M,V,C,将能够把相同的逻辑职责尽可能集中到一起来管理,从而极大地增加客户端代码的可维护性和可测试性。

下表简单对比服务端和客户端MVC下M,V,C的对应职责:

 

Model

View

Controller

Server Side 返回用于呈现页面内容的数据的 Domain Objects 代表了一个页面的抽象,包括页面的内容呈现,数据,事件定义 处理View上触发的事件,获取数据,更新View上的数据,触发View的内容呈现
Client Side 返回 JSON 数据的 Restful Services 同上 同上

最佳实践二:应用依赖注入和IoC容器

应用MVC模式,本质上是抽象的逻辑职责上的解耦。而依赖注入和IoC容器则是代码的物理依赖性上的解耦。尽可能的利用构造器注入,设值注入,接口注入或IoC容器来解除具体的实现类之间的直接依赖,自然就能极大的大提高每个具体的实现类的可测试性。

最佳实践三:应用模板引擎呈现主体内容

AJAX应用中的一个需要客户端呈现的View,必然需要呈现一些HTML,这些HTML往往需要根据Model返回的JSON数据动态构造。一般来讲,我们会有三种方式来构造和呈现这些HTML:

  • 在JavaScript中遍历JSON数据,拼接HTML字符串,呈现到页面上;
  • 在JavaScript中遍历JSON数据,动态实例化DOM对象,通过DOM对象的方法,呈现HTML的DOM;
  • 通过如JTemplate这样的JavaScript模板引擎,将JSON数据绑定到一个HTML模板,由模板引擎呈现最终的HTML;

本最佳实践的建议内容就是,对于一个View的主体内容,应该尽可能的通过模板引擎来呈现。为什么呢?因为,对于一个WEB程序来说,最不稳定的,会经常变化的部分,无疑是纯图形的HTML和CSS,使用模板引擎,将能够使得这些HTML尽可能的集中,并且易于修改,也更易于HTML和JavaScript的整合。

最佳实践四:应用Script#

应用Script#好处前面已经提过了,这里再简单列举一下:

  • 充分利用现有的IDE对流行的编程语言如C#源代码的完善的coding,尤其是强类型,重构和测试的支持;
  • 相对于高薪聘请Senior的JavaScript开发人员,Junior的 C#的开发人员要便宜得多;

如反对,请列举我不该用它的理由?

 

对于以上几个最佳实践的应用实例,请参见我之前的文章:This is jqMVC# – CNBLOGS Google Tracer Sample

 

欢迎补缺、指正!谢谢!

posted @ 2010-07-11 22:23 Teddy's Knowledge Base Views(2579) Comments(24) Edit 收藏

 回复 引用 查看   
#1楼2010-07-11 23:33 | 亚历山大同志      
相对于高薪聘请Senior的JavaScript开发人员,Junior的 C#的开发人员要便宜得多;
--------------------------------------------------------------------------------
这句话要伤很多人的心啊

 回复 引用 查看   
#2楼2010-07-11 23:35 | 阿不      
普通的TDD in C#都还没有能够很好的实践,对于TDD in HTML&Javascript就更加的没有想法了。不过还是非常期待能有好的讨论。
另外,对于Script#的应用,仍感到一定的怀疑。几年前看这个东西的时候,那时候javascript刚被重视,但目前我想,如果要做好web开发,javascript是一门必不可少的语言,包括它的核心,动态性以及OO的实现。我了解,teddy希望用Script#并不在于它能代替javascript,带来多大的javascript透明开发,而是之于它静态维护方面的优势。

先休息,明天再来关注。

 回复 引用 查看   
#3楼2010-07-12 04:13 | 阿毅      
java平台是绝对可行的,看看Google Web Toolkit就知道.但.net平台还需观望,对Script#不抱那么高期望,jsc更是没接触过,了解一下再说.
我也认同你说的"相对于高薪聘请Senior的JavaScript开发人员,Junior的 C#的开发人员要便宜得多;",但我觉得,更多人需要的不是Script#而是.net版的"Google Web Toolkit",是需要比较完整,比较一体化的解决方案.

 回复 引用 查看   
#4楼2010-07-12 06:34 | 布尔      
@亚历山大同志
呵呵,没关系,越来越多的人即具有C#技能又具备Js技能。

 回复 引用 查看   
#5楼2010-07-12 08:25 | LanceZhang      
Script#真的有那么NB,能让一个junior的C# developer写出senior的js吗?
 回复 引用 查看   
#6楼2010-07-12 10:25 | Nick Wang (懒人王)      
best practice的根本问题在于,绝大多数人看重的都是“solution”,而不是最重要的“context”。而且在这些best practice之中,往往“solution”要写好几页,而“context"却只写一两行

IoC
javascript不需要ioc,因为本身就是弱类型的,直接做alias就行了。比如:
//definition
function ieVersionF(){}
function otherVersionF(){}
var f = isIE? ieVersionF : otherVersionF;
//usage
f(); //invoke function

模版引擎
不是特别了解,不知道会不会有性能问题。

另外,似乎上面讨论的只限于将用ajax拿到的数据做呈现。如果只是显示异步取得的数据的话,逻辑比较简单。复杂的是富浏览器应用,这时web就和desktop程序的结构类似了。HTML+CSS是UI,javascript就是行为,ajax + json就是存储接口,服务器端可以做验证,可以有逻辑,也可以纯粹就是database。比如现在流行的js写的web游戏。

 回复 引用 查看   
#7楼[楼主]2010-07-12 10:45 | Teddy's Knowledge Base      
@阿毅
我并不排斥Java,即使是基于Java的解决方案,如果能解决我的问题,我也不介意采用。但是事实上,Google Web Toolkit只是Google自己自用的一个方案,并不能作为一个可复用的架构,用于我自己的封闭项目。我的目的是使用现有的任何技术,来解决问题,如果没有现成的理想的整体方案,我不介意创造出一个可行的方案,这个方案未必是完全出于一家的技术,但是要能解决问题。

 回复 引用 查看   
#8楼[楼主]2010-07-12 10:49 | Teddy's Knowledge Base      
@LanceZhang
当然,完全不了解JS的C# Dev不可能写出好的JavaScript代码,即使通过Script#。但是,只需要稍微了解JS的C# Dev,确实可以在Senior的C#和JS Dev的引导下,通过Script#用高得多的效率写出理想的JavaScript代码,总体的成本比全都用JS Dev要小得多得多。

 回复 引用 查看   
#9楼2010-07-12 10:51 | 亚历山大同志      
@Nick Wang (懒人王)
现在纯js写游戏等于找死阿,不同浏览器的兼容性和效率的优化就要做死人,现在大部分都直接上flex了

 回复 引用 查看   
#10楼2010-07-12 11:03 | Nick Wang (懒人王)      
@亚历山大同志
效率和兼容性确实是js目前最大的问题。不过apple已经不支持flash了,silverlight根本就没占据主流市场,不知道javascript是不是未来的天子啊。

 回复 引用 查看   
#11楼[楼主]2010-07-12 11:05 | Teddy's Knowledge Base      
@Nick Wang (懒人王)
离开context,任何solution就都没有意义了。皮之不存,毛将焉附?你说的“best practice的根本问题在于,绝大多数人看重的都是“solution”,而不是最重要的“context”。而且在这些best practice之中,往往“solution”要写好几页,而“context"却只写一两行”我完全无法赞同。

你说“JavaScript不需要IoC”,因为“本身就是弱类型的”,这个是完全站不住脚的,甚至说明你还不了解依赖注入和IoC的本质,通过一个回复很难说明请这个问题,如有疑惑,我们可以私下联系,或者我另起一文来说服你。

模板引擎的错误使用确实会有性能问题的。但是,请注意,我文中特别用黑体提示的“对于一个View的主体内容,应该尽可能的通过模板引擎来呈现”,而不是任何时候都用模板引擎。

“复杂的是富浏览器应用,这时web就和desktop程序的结构类似了”- 没错,现在的AJAX富应用就是越来越趋向于desktop程序了,这也正是更多后台代码的技术和桌面程序的技术,会,也应该越来越多的应用于javaScript代码的原因。

 回复 引用 查看   
#12楼[楼主]2010-07-12 11:11 | Teddy's Knowledge Base      
@亚历山大同志
对于游戏,目前确实绝大多数情况下JS还不太合适。不过未来随着高性能的JS引擎的普及和html标准对3D和硬件加速的支持,完全基于JavaScript和HTML标准的复杂游戏,也应该还是大有希望的。

 回复 引用 查看   
#13楼2010-07-12 11:29 | Mainz      
MVC,IoC都主要是服务器端的东西吧,和js有啥关系。既然是服务器端就不难测试了。难点就在前端。

前端测试我用过Selenium自动化测试工具(测Ext专用的),楼主可以自己去开发一个适合你们项目的自动化测试工具,参考下Selenium ~ 自己开发才是王道!要不然就花钱让别人测试!

 回复 引用 查看   
#14楼2010-07-12 11:30 | Nick Wang (懒人王)      
@Teddy's Knowledge Base
你说“JavaScript不需要IoC”,因为“本身就是弱类型的”,这个是完全站不住脚的,甚至说明你还不了解依赖注入和IoC的本质,通过一个回复很难说明请这个问题,如有疑惑,我们可以私下联系,或者我另起一文来说服你。

你怎么知道我不了解IoC?事实上我不在乎你认为我了不了解,我也不在乎我自己到底真的了不了解。问题是,为什么你的每个回复都要试图说服我,说服我同意你的看法?

讨论的意义不在于得出一个结论,得出什么是对,什么是错,而是通过不同的看法、不同的观点来扩展思路。因此,每个人只要说出自己的疑问、看法、观点就行了。至于是否有人认同,是否有人从中获益,那不是参加讨论的人能控制的,也不是参加讨论的人应该控制的。

我说的话,写的字,本就没有想要说服谁。如果对你有益,很好;如果对你无益,至少也无害。如果你不认同,想要帮助我,那非常很好,我也很感谢。只要说出你的观点和质疑就行了,至于我是不是认同,是不是被说服,不应该成为讨论的目的。

前段时间的C#大争论也是一样的,某个人写了C#这里这里不好,就有一帮人冒出来逐一反驳。你写一个不好,我就要证明它好。如果只是为了讨论它是不是真的好也就罢了,可是各位的真正目的仅仅是讨论事实,讨论技术么?对于大多数人来说,目的只是为了说服别人认同自己的观点。

 回复 引用 查看   
#15楼[楼主]2010-07-12 11:40 | Teddy's Knowledge Base      
@Mainz
“MVC,IoC都主要是服务器端的东西吧” - 谁说他们就一定是Server端的?设计模式本身没有服务端或客户端的区分的。

“Selenium”其实就是我提到的“传统的只能模拟需要get或post的数据的测试工具”。

 回复 引用 查看   
#16楼2010-07-12 11:45 | Mainz      
@Teddy's Knowledge Base
你说的没错,设计模式本身没有服务端或客户端的区分的。但我还没在项目中实践过MVC的JS框架。你做过吗?性能如何?

 回复 引用 查看   
#17楼[楼主]2010-07-12 11:55 | Teddy's Knowledge Base      
@Nick Wang (懒人王)
兄弟,别那么激动,我自问绝没有不尊重你的意思,就事论事而已。

我的所有回复都只是针对你说的某一句话,并不代表对你这个人的评价。比如你说“JavaScript不需要IoC”,因为“本身就是弱类型的”,对于这句话,根据常识,是一个假命题。说弱类型就不需要IoC我认为是错的。我说了,我有信心能说明为什么我我这么说,如果需要更多解释,我甚至可以另其一篇文章来说明。但是,如果我觉得谁说的不对,难道我不能纠正吗?如果大家的讨论无论对错,不讨论一下是非,那么,对其他不明对错的人,就不公平了。当然,有些问题,不可能有绝对的对错,这个是可以理解的,博采众长即可,我也不会过多纠缠,但是对于常识错误,不纠正一下就是对大家的不负责任了。

 回复 引用 查看   
#18楼2010-07-12 12:02 | Nick Wang (懒人王)      
@Teddy's Knowledge Base
你说“JavaScript不需要IoC”,因为“本身就是弱类型的”,这个是完全站不住脚的,甚至说明你还不了解依赖注入和IoC的本质,通过一个回复很难说明请这个问题,如有疑惑,我们可以私下联系,或者我另起一文来说服你

当然,有些问题,不可能有绝对的对错,这个是可以理解的,博采众长即可,我也不会过多纠缠,但是对于常识错误,不纠正一下就是对大家的不负责任了。

你怎么就知道你的纠正就是正确的呢?我不是跟你吵架或者是较真。只是我觉得不可能得到一个结论--那个是对的,哪个是不对的。

每个人都是可以为自己负责的成年人,如果某人说了什么,你相信了,过后发现不对,那也是你自己的问题,谁叫你相信了呢?这个是怪不得别人的。

因此作为写这些东西的人,也要抱着这种态度--我写我认为对的东西,你可以反驳我,我也可以反驳你,但是我不能保证我写的东西是绝对正确的,相不相信由你。

 回复 引用 查看   
#19楼2010-07-12 12:15 | Tony Qu      
不错!不过我还是觉得js的tdd就应该用js来写
 回复 引用 查看   
#20楼[楼主]2010-07-12 13:37 | Teddy's Knowledge Base      
@Nick Wang (懒人王)
如我所说“有些问题,不可能有绝对的对错,这个是可以理解的,博采众长即可,我也不会过多纠缠,但是对于常识错误,不纠正一下就是对大家的不负责任了。” - 我倒不是说我说的一定正确,但是,我想表明的是,有些具体的问题,是可以说出对错的,那么就值得讨论出一个正确的观点。

比如,.NET suck还是Java suck的问题,自然没必要争论。但是,IoC的概念确实明摆着的,除非我指的ioc和你指的ioc不是同一个东西,否则,为什么就不能分辨谁说的对呢?BTW,我说的ioc指的是:http://en.wikipedia.org/wiki/Inversion_of_control

至于你说的“我写我认为对的东西,你可以反驳我,我也可以反驳你,但是我不能保证我写的东西是绝对正确的,相不相信由你”这样一个态度,我不作评论,尽管我不一定认同,因为,这个话题确实是仁者见仁的,每个人有自己的处事风格,无所谓好坏对错的。

 回复 引用 查看   
#21楼2010-07-12 13:39 | 镜涛      
@布尔
应该是一种基本技能..

 回复 引用 查看   
#22楼2010-07-13 15:59 | ﹎敏ō      
@Teddy's Knowledge Base
开源又何来自给自用,封闭?
而且我觉得你应该先了解一下"Who's Using GWT"再说这些话...

 回复 引用 查看   
#23楼[楼主]2010-07-13 17:10 | Teddy's Knowledge Base      
@﹎敏ō
GWT限制了你必须使用它的架构,因此,缺乏灵活性。虽然它本身是开源的,但是,架构上的限制限制了他的应用范围,比如,我想使用我希望的JS类库,如JQuery,或者整合我自己的类库到Java源代码,貌似没那么方便。

相对的,类似Script#的机制则更灵活,Script#仅仅是将C#转换为JavaScript,换句话说,我用Script#也仅仅是(也正是我希望的)帮我更高效的写JS,而且它能用C#方便的包装任何的第三方JS库,因此,我可以灵活的搭建我需要的架构。

 回复 引用 查看   
#24楼2011-05-05 23:14 | banana.totolv      
good