让我们再聊聊TDD 续 – 人人都在做TDD

上一篇文章里面,通过对DHH的文章以及DHH和Kent Beck等讨论的分析,我阐述了对TDD的理解和分类,现在来继续聊聊TDD的实施和分层。

现在还有非常多的软件工程师在质疑TDD的可行性,比如太难不会、成本太高无法推动、意义不是很大等,但是他们却一直都在做着TDD,只不过没有意识到而已,这便是“不识庐山真面目,只缘身在此山中”。

TDD的实施一般分为思维层面和技术层面。一般来说,思维层面上的实施成本较低、容易接受,但是缺点很多,比如难以传递、难以持续获得快速反馈等;而技术层面上的实施一般成本较高、不容易被人接受,但是优点更多,比如可以获得快速反馈、更容易传递和协作等。而现实世界中TDD的实施一般分为三个阶段,即无意识的TDD、被动通过技术实现的TDD、以及有意识和主动通过技术实现的TDD。

第一阶段:无意识的TDD

对于软件开发人员,当他们拿到一个新的软件需求时,首先会思考如何实现,其中包括当前软件架构、业务分解、实现设计、代码分层、代码实现等。然后通过思考和设计所得到的产出物来驱动代码实现,进而在代码实现中会思考如何通过一个或多个函数或者算法来实现业务逻辑。所以软件系统的实现要先通过意识层面的思考,再进行技术层面的工作。

当开发人员思考和设计这些函数或者方法的时候,一般都会思考它们有哪些参数,然后想象将这些参数换成真实的数据后传递进去,会得到怎样的返回值。好一点的开发人员会思考如何处理异常输入和异常返回值。

这类思考其实已经是意识思维上的TDD,它帮助开发人员先在大脑里面设计并验证代码实现,甚至帮助其重构代码。所以很多开发人员都在无意识的情况下做着TDD。

比如在一个银行系统里面,开发人员拿到一个需求,需要开发一个通过手机APP转账的功能。

  1. 首先开发人员会基于当前的软件架构思考:是开发一个全新的模块来处理这个业务?还是基于当前架构中的某个模块来添加代码进行处理?
  2. 当确定架构和设计之后,就开始思考具体的代码实现,比如类的设计、方法的设计或者函数的设计等。当开发“将钱从原帐号转出”这个功能前,开发人员会思考:这个功能需要支持当钱从原帐号中转出成功后,原帐号中的余额等于原始余额减去转出金额。进一步有些程序员还会设计一些用来验证功能的实例,比如帐号中的原始余额是999.99,转出111.11,那么剩余的金额就应该是888.88。
  3. 在这样思考之后,开发人员便开始根据自己大脑中的测试逻辑和用例来驱动和辅助开发过程。在代码开发完毕之后还会想一些办法来验证一下所实现的功能是否符合预期,比如人工使用之前的或者新的测试用例再测试一下。如果验证正确,就会认为自己开发的功能正确了,并交给测试人员进行测试。

其实开发人员在开发前思考测试逻辑和用例的过程就是在做TDD了。

很多做业务分析的BA和测试分析前移的QA也同样在无意识的做着TDD(注:在前一篇文章中已说明TDD包含ATDD),比如分析验收条件、写出验收文档等。只不过这些AC和验收文档可能写得不是很明确或者不是很好,比如不是实例化需求等,但是本质上已经是TDD了。

只不过是初级的无意识的TDD,可能有的人做得好,有的人做得不好,而且没有明确的产出来协助和规范这个测试驱动开发方式,也缺乏快速反馈、度量、传递和协作等。因此从无意识到有意识将是做好TDD的一个重要过渡。

第二阶段:被动通过技术实现TDD

当有一部分软件工程师意识到了TDD的意义和普遍存在性之后,就开始准备解决思维上的TDD的缺点。而解决这些问题的方法就是在技术层面上用代码来实现TDD,用明确的代码来协助和规范开发人员的测试驱动开发行为,来度量他对业务逻辑以及代码实现的理解度。通过将他的理解传递给以后的维护人员,让他的理解能重复被使用,以及和其他人协作开发。

但是现实中很多开发人员的认识不足以及技术能力不够,就算管理层支持并且主动推动TDD,最终由于开发人员设计和选取的测试用例合理性很差,导致驱动出来的代码有效性差,测试用例无法体现出SBE(Specification by example)导致易读性差,对于自动化测试框架和测试编写不熟悉导致开发速度很慢等,往往是被动的在技术层面上去实现TDD,所以出现了各种怨言,各种抵触,进而导致技术层面上的TDD很难以大规模实施。

由于意识层面上的难易程度和工作量都比技术层面上相对较小,所以前者实施起来相对容易一些,而后者则相对较难,所以如果通过了各种手段强行实施TDD,而没有主动去摆正做TDD的意识,甚至没有足够的技术能力,那么这样的TDD就是一个倒三角,非常容易倒塌。

TDD倒三角

所以,如果不希望技术层面上的TDD随时倒塌,就需要把这个倒三角补全,才能更好的、长久的实施TDD。

第三阶段:有意识和主动通过技术实现TDD

为了大规模以及有效的实施TDD,首先要突破思维意识的局限,认识到TDD的普遍存在性和适用性,不要害怕和排斥TDD这种思维和开发模式。

其次要主动学习,并刻意练习TDD的技术实现,提升自己的技术能力,从而在技术层面能更容易的实现TDD,摆脱被动TDD的困境。其中学习的方法包括阅读TDD相关的书籍和文章,书籍包括《测试驱动开发》、《重构》、《BDD In Action》以及《系统思考》等,从而充分理解TDD优点和局限。

对于刻意练习,一定要长时间坚持去做,让其成为一种习惯。如果在项目中没有合适的环境去练习,还可以通过一些第三方的TDD练习系统去做刻意练习,比如Cyber-dojo。只有大量的刻意练习才能让你在真实的代码编写过程中去思考和理解TDD,去运用你通过学习得到的知识,最终才能做到有意识和主动的通过技术去实现TDD,TDD的倒三角才能变成一个稳定的砖块,然后哪里需要往哪里搬。

TDD砖块

总结

综上,大部分开发人员都应该在做TDD,只不过他们是无意识的或者被动的去实现的,只有少部分是有意识和主动的去实现的。既然人人都在做TDD,那么我们为什么不能和黑客帝国里面的Neo一样选择红色药丸来认清楚现实,主动拥抱TDD,并通过大量的刻意练习去改变自己的工作习惯,让TDD成为自己工作习惯的一部分,这样才能更好的提升软件质量,大大降低软件维护成本。不管你信不信,反正我信了。


更多精彩洞见,请关注微信公众号:思特沃克

Share

让我们再聊聊TDD

最近几年“TDD已死”的声音不断出现,特别是David Heinemeier Hansson那篇文章——《TDD is dead. Long live testing. (DHH)》引发了大量的讨论。其中最引人注目的是Kent Beck、Martin Fowler、David三人就这个举行的系列对话(辩论)——Is TDD Dead?

当前国内对TDD的理解十分模糊,大部分人也没有明确和有意识的去实施TDD,因此许多人对此都有着不同的理解。

其中最经典的理解就是基于代码的某个单元,使用Mock等技术编写单元测试,然后用这个单元测试来驱动开发,抑或是帮助在重构、修改以后进行回归测试。而现在大部分反对TDD的声音就是基于这个理解,比如:

  • 工期紧,时间短,写TDD太浪费时间;
  • 业务需求变化太快,修改功能都来不及,根本没有时间来写TDD;
  • 写TDD对开发人员的素质要求非常高,普通的开发人员不会写;
  • TDD 推行的最大问题在于大多数程序员还不会「写测试用例」和「重构」;
  • 由于大量使用Mock和Stub技术,导致UT没有办法测试集成后的功能,对于测试业务价值作用不大
  • ……

总结一下,技术人员拒绝TDD的主要原因在于难度大、工作量大、Mock的大量使用导致很难测试业务价值等。

这些理解主要是建立在片面的理解和实践之上,而在我的认知中,TDD的核心是:先写测试,并使用它帮助开发人员t来驱动软件开发。

首先是先写测试,这里的测试并不只是单元测试,也不是说一定要使用mock和stub来做测试。这里的测试就是指软件测试本身,可以是基于代码单元的单元测试,可以是基于业务需求的功能测试,也可以是基于特定验收条件的验收测试。

其次是帮助开发人员,主要是帮助开发人员理解软件的功能需求和验收条件,帮助其思考和设计代码,从而达到驱动开发的目的,所以TDD是包含两部分:ATDD与UTDD

ATDD(Acceptance Test Driven Development):验收驱动测试开发,首先BA或者QA编写验收测试用例,然后Dev通过验收测试来理解需求和验收条件,并编写实现代码直到验收测试用例通过。

由于验收方法和类型也是多种多样的,所以根据验收方法和类型的不同,ATDD其实是包含BDD(Behavior Driven Development)、EDD(Example Driven Development),FDD(Feature Driven Development)、CDCD(Consumer Driven Contract Development)等各种的实践方法。

比如以软件的行为为验收标准,这个是BDD;如果以特定的实例数据为验收标准,这个是EDD;如果以Web Service API消费者提出API契约来驱动API提供者开发API,这个是CDCD等。所以ATDD的具体实现需要结合项目的实际情况来选用适合的验收测试方法与类型。

UTDD(Unit Test Driven Development):单元驱动测试开发,首先Dev编写单元测试用例,然后编写实现代码直到单元测试通过。这个就是现在很多人所谓的TDD、实践的TDD、喜欢的TDD、抱怨的TDD,但是它却只是真正意义上TDD的一部分而已。

TDD金字塔

再来看看David 的《TDD is dead. Long live testing》,他主要是认为TDD大量使用mock,导致无法测试软件连接了数据库之后的功能,进而无法测试其业务价值。

其次他提出应该使用”Long live testing”, 而他并没有说明这种测试应该是在编写代码之前还是之后写,以及会不会用来作为客户对于软件的验收标准。如果他没有这样做,那他只是使用”Long live testing”来做回归测试;如果他做了,那么他也是使用了ATDD,从而使用了TDD。

所以他对TDD的理解还是狭隘的,认为TDD只是UTDD,导致他写了这篇文章来批评TDD。有可能他在现实工作中已经使用了ATDD,也就是TDD。

最后来看看Kent Beck、Martin Fowler、David关于Is TDD Dead?的辩论,我觉得他们所说的都有道理,并且也是合理的。原因是他们的背景和行业不同,本来对于不同的行业和不同的背景就应该选择适合的测试驱动方法(有可能不一样)。

首先来看看Kent Beck,他在Facebook工作,出版过很多书,可以定位为一名在大型IT公司工作的软件思想家。其次是David,一个标准欧洲帅哥,ROR创造者之一,Basecamp公司的创始人和CTO,Basecamp是一个只有几十个人的小型软件公司,所以他可以定位是一名创业者、技术牛人。

Kent Beck所在的公司开发的是大型复杂业务软件(Facebook平台),代码量巨大,需要长时间(几年)大量人员(几十甚至几百)来开发和维护。DHH开发的中小型企业软件(比如CRM),代码量一般,需要快速(几个月)、少量人员(几个到十几个)开发和维护。

Kent Beck在金钱和人力资源相对充足、时间相对充裕的情况下追求的是代码质量,大量人员的良好协作与平台稳定。DHH却在金钱和人力资源相对较少情况下追求最大化客户业务价值,使得少量人员能快速开发出软件并卖给客户赚钱。

所以在Kent Beck所在的环境下,单元测试(UTDD)是非常有价值的;而在DHH所在的环境下,功能测试或者ATDD却更为适合。

国内很多人对于TDD的狭隘理解还源于很多网上的中文资料,百度百科对于TDD的解释就是其中一个:

“TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD虽是敏捷方法的核心实践,但不只适用于XP(Extreme Programming),同样可以适用于其他开发方法和过程。”

而国外有不少站点上的资料是对于TDD是有正确理解的,比如下图是一个敏捷调查表。从其中的“We take a test-driven development(TDD) approach”和”We take a TDD approach at the requirements level”就能发现其对TDD的理解就是包含UTDD和ATDD。

TDD不是银弹,不要期望它能轻易解决你的问题,无论是UTDD、EDD还是BDD,根据自己项目的实际情况,比如资金、人力资源、时间、组织架构等,合理的选择。

今天我们又聊了聊TDD,也希望大家重新理解一下,重新思考和尝试一下,然后你会发现另外一片云彩。

TDD并没有死,死的是你的持续学习、思考、实践与总结。当前国内很多软件开发人员对于TDD的理解比较模糊,大部分人也没有明确和有意识的去实施TDD,因此很多人都有着不同的理解。


更多精彩洞见,请关注微信公众号:思特沃克

Share

从测试策略到测试架构

文/刘冉

今年是我做软件测试的第7个年头了,当年我从软件开发转做软件测试的时候,没有想过我能在这个领域做这么久。

在这7年里面,我在软件测试领域摸爬滚打,从自动测试起步,逐步接触到软件测试的各个领域:各种测试方法(等价类,全配对等)、测试技术(单元测试,功能测试,性能测试,探索性测试等)、自动化测试工具(JUnit,Selenium,Gatling,ZAP等)、测试流程(传统测试流程,敏捷测试流程等)以及测试策略(测试象限和测试金字塔等)。

其中“测试策略”在测试业界是讨论的比较少的,因为大多数人的工作重点是设计测试用例,执行测试或者开发和维护自动化测试,而只有少部分人才会涉及到测试策略的工作,从而导致很多测试人员其实并没有系统的了解测试策略。

所以我准备将我这几年对于测试策略的经验、总结以及思考以系列文章的形式写出来,希望能稍微帮助一下大家去理解测试策略,从而做到更好的测试,减少缺陷,提高质量。

测试策略

首先来看一下Wikipedia上对于测试策略的定义:

A test strategy is an outline that describes the testing approach of the software development cycle. It is created to inform project managers, testers, and developers about some key issues of the testing process. This includes the testing objective, methods of testing new functions, total time and resources required for the project, and the testing environment.

Test strategies describe how the product risks of the stakeholders are mitigated at the test-level, which types of testing are to be performed, and which entry and exit criteria apply. They are created based on development design documents. System design documents are primarily used and occasionally, conceptual design documents may be referred to. Design documents describe the functionality of the software to be enabled in the upcoming release. For every stage of development design, a corresponding test strategy should be created to test the new feature sets.

更多内容详见:https://en.wikipedia.org/wiki/Test_strategy

所以测试策略(Test Strategy)的第一目标就是“减少缺陷的出现和发布”。其中“减少缺陷的出现”可以通过测试前移等方法来解决,在进行软件需求分析和架构设计的时候发现缺陷;而“减少缺陷发布”可以使用各种测试方法、技术来验证和测试编码完成的功能(这两点在今后的文章里面会通过不同的例子进行更详细的阐述)。

由此可见,“测试策略”并不是只由测试人员定制的,它是由一个团队的各个角色一起来制定和建立的,目的是保证软件的质量,减少缺陷。

而“测试计划”是用于实施测试策略的。只有充分理解测试策略目的和实施方式,才能充分理解测试策略,为什么要做测试策略,什么样的测试策略才更有意义、更好,怎样实施才能更有效等问题。

测试计划

测试计划在Wikipedia中是这样定义的:

A test plan documents the strategy that will be used to verify and ensure that a product or system meets its design specifications and other requirements. A test plan is usually prepared by or with significant input from test engineers.

更多内容详见:https://en.wikipedia.org/wiki/Test_plan

制定测试计划是保证测试策略能被有效执行的一种方式。它告诉了团队在什么阶段,什么样的角色应该执行测试策略中什么样测试技术和测试方法。它主要由测试人员编写,但是应该由整个团队进行评审,因为开发人员、产品经理、业务分析人员甚至用户都可能参与到测试计划的执行中。

测试计划是可以根据项目的实际进展情况进行调整的,所以它并不是一成不变的。

测试架构

在上个世纪六七十年代软件系统还处于小规模的时候,软件开发并没有谈什么架构,软件测试也不存在什么策略可言。但是随着软件规模的极速增大,复杂性也成指数级增加,专业的软件架构应运而生。

为了有效的在规定时间内完成复杂软件系统的测试,必须有一个指导性的策略来帮助团队理解、选择和组织大量的测试,因此软件测试策略就出现了。而测试策略往往是高层次的指导,对于一些中小型项目也许已经足够了,但是却不足以应付现代越来越复杂的软件系统。

因为随着微服务、移动互联网、物联网、大数据分析系统、AI系统等的出现,要测试一个包含各种技术,外部依赖,或者独立子系统的复杂系统,并不是简单的根据测试策略在不同层面上做不同的测试就可以了,而是要理清各种测试之间的相互联系和制约,然后思考怎么有效的将各个维度上的测试联系起来,以软件系统架构的思维去思考整个测试体系。

请注意这里不是说要去设计一套全自动化的测试系统来完成整个系统的所有测试,而是通个各种有效的方式(无论手动还是自动)把各种测试合理且有效的联系起来,形成一个拥有完整架构的测试体系,这样才能使整个系统的各种测试更加可视化和更易于理解,使整个系统的各种测试更加有效,避免重复测试,节约成本。

举例来说,一个前后端分离的Web业务系统不仅有前端UI和大量的JavaScirpt代码,还有后端的API和第三方依赖系统以及数据库系统,如何将各层测试有效的联系起来就是测试架构需要解决的问题。

首先,前端、后端API、第三方依赖系统和数据库系统有各自的单元测试、集成测试等,然后可以使用契约测试来测试统一前端和后端API,再使用Stub加入对于第三方依赖系统的契约测试或者监控测试,还需要使用测试数据生成系统参数,将各种测试数据存入数据库系统用于支持契约测试等。

对于不同软件系统,其架构一般都是根据业务需求、技术能力等各种条件来设计的。与软件架构一样,测试策略和测试架构在不同的项目里面,需要根据其软件系统的架构、技术栈、业务需求、人员的技能等因素来定制和设计。

再谈测试策略

现在业界流行的测试金字塔和测试象限只是两种高度抽象和简化的测试策略模型,不具备实际可操作性,只具备高层次的指导性和参考性。直接根据这两个模型来工作是低效的,甚至可能带来负面效果。所以对于测试金字塔和测试象限不能盲目的使用,而是需要根据项目的实际情况来生成适合自己项目的测试策略和测试架构,并在此基础上执行真实的测试工作。

扩展阅读:

http://www.infoq.com/cn/articles/an-effective-test-strategy

http://www.testingexcellence.com/test-strategy-and-test-plan/

更多精彩洞见,请关注微信公众号:思特沃克

Share

XCodeGhost随想

昨天一条XCodeGhost的消息在微信朋友圈和IT圈炸开了锅,并且引发了许多人的愤怒和担忧,愤怒的是中国这么多知名IT公司不负责的开发行为,担忧的是自己的用户名密码等隐私信息的再一次泄露将给自己带来又一次的伤害。

简单说XCodeGhost是有人在XCode里面植入了一段代码,导致用户使用通过这个XCode编译的iOS应用软件时会自动执行这段额外植入的代码,并将用户的隐私信息发往黑客的服务器,详细信息参见乌云知识库。截至今天已经有超过20款iOS应用确认拥有XCodeGhost。

当我看到这个新闻的时候,第一个想法就是”历史总是如此的相似”。让我们回到上个世纪70,80年代,Unix之父Ken Thompson(1943~)在贝尔实验室里面开发了Unix系统,并且贝尔实验室的计算机也安装了这个Unix系统供员工使用。有一天大家发现Ken总是可以以最高权限进入他们的帐号。可想而知,在牛人聚集的贝尔实验室是不允许这样的事情发生的,所以许多员工跳出来仔细分析Unix代码,并且确实也找到了后门,然后修复这些后门后重新编译整个Unix并重新安装。所有人都以为可以放心的使用新的Unix的时候却发现Ken还是可以自由的获取他们的账户权限,并且再也无能为力了。直到多年以后,Ken才解开了大家心中的疑惑,原来Ken在编译Unix所使用的C语言编译器里面加入了一个后门,而当时跳出来分析和修复Unix的员工并没有去分析过C语言编译器,导致Ken一再得手。回想昨天的新闻,竟是如此的相似。

相对于Ken在C语言编译器植入后门,XCodeGhost在XCode里面植入后门只能算小儿科(因为当时贝尔实验室使用的C语言编译器是Ken开发的,相当于官方发布了具有后门的编译器),毕竟官方的XCode并没有包含XCodeGhost。那么为什么还是有如此多的知名IT公司的iOS应用都有的此后门啦?其中有一个传言就是由于从中国去Apple官服下载XCode非常慢,所以需要去各个社区或者通过迅雷下载,所以才导致下载到了包含XCodeGhost的XCode。我觉得迅雷纯属躺枪,开发者使用了不安全的工作方法,还要找个借口发泄到迅雷身上,这本身就是一种不负责的态度。

对于XCodeGhost的爆发,我有一些疑惑:

  • 这些公司的软件发布流程是怎么样的?是通过CI(持续集成)统一构建发布,还是随便找一个开发人员,从其开发机上构建发布?
  • 如果是使用CI统一发布,难道CI配置也是随便配置,没有任何规范流程?

我认为良好的CI环境是预防XCodeGhost这样的漏洞最好方法,因此我们应该做到:

  • 软件的发布版本一定要通过CI构建获得,并且CI环境中构建所使用到的软件一定要从官方渠道获得。
  • 代码版本库里面最好只有源代码,如果需要包含依赖库,这个依赖库也只能通过CI构建,而不是在某个开发的机器上构建获得。

其次:

  • 公司应该规定开发使用的软件都应该从官方渠道获取,并要求对于下载的软件进行MD5或者SHA-1哈希值验证。
  • 如果开发人员过多,不方便管理,可以要求IT统一从官方驱动获取软件,然后由IT统一管理和安装所有软件。

扩展随想,编译器/IDE以后我们注意了,第三方包/框架我们就可以放心了吗?这些第三方包就不会有类似XCodeGhost的后门?针对这个我有一些想法:

  • 尽量使用在线包管理系统下载第三方包,现在基本上大部分的语言都有其第三方包在线管理方案,java有maven,ruby有gems,nodejs有npm,iOS有Alcatraz和CocoaPods等等。并且在使用前尽量通过这些在线包管理系统多了解一下你将使用的包,比如下载量,使用者评论,未修复的bugs等。
  • 在CVE,NVD,乌云等这类安全漏洞网站上查询你是用的第三方包是否存在漏洞。如果你觉得手动麻烦,还可以使用OWASP Dependency Check来自动扫描你将使用的第三方库,从而及早发现其漏洞,详细使用方法参见在CI中实现持续Web安全扫描

总的来说,一个应用软件的安全可以从五个角度考虑:操作系统,病毒,源代码(包括软件架构和设计),编译器/IDE,第三方软件包,见下图:

1

Share