写点什么

TDD 实践之实用主义

  • 2008-10-20
  • 本文字数:4572 字

    阅读完需:约 15 分钟

TDD 的概念和实践依然在被怀疑和争议。然而,质疑者驻足不前,实践者却不再理会争论的口水,而是脚踏实地的实践,并获取高效回报。在这个过程中,我们发现 TDD 和敏捷倡导的理念,帮助我们解决了一个又一个的工程问题。

1. 为沟通选择语言

我们在一个海员管理系统的开发中遇到了问题,这个领域的专业术语我们很难翻译。即使勉强翻译出了,也感觉辞不达意,无论是初看上去,还是过一段时间再看都一头雾水。比如,我们写出了下面的测试用例:

public void test_should_return_NOT_pass_if_duty_higher_than_second_mate_or_second_engineer_and_education_level_is_secondary_and_guraduated_after_2002_02_01() {<br></br> ……<br></br>}但其中second mate/second engineer是什么意思呢? secondary 的 education level 具体又是什么?

还有:

public void test_should_return_third_mate_course_for_jianxi_third_mate() {<br></br> ……<br></br>}``jianxi_third_mate是什么? 等等。

当然,我们可以制定一个术语表,请专业人士先帮我们翻译好,然后在代码中遵循这个术语表。然而随着需求的增加,术语层出不穷,并且有特定中国特色的名词根本就没有对应的翻译,于是这个问题就一直困扰着我们。

而直到有一天,在一次重构中我们把上面的第一个测试用例重命名了一下,一切似乎突然间开朗了:

public void test_ 应该算未通过 _if_ 职务高于二副二管轮 _ 而 _ 学历只是中专 _ 并且 _ 毕业时间晚于 2002 年 2 月 1 日 () {<br></br> ……<br></br>}Team 里的人纷纷围过来,看着这个跟需求描述里的验收条件几乎一模一样的测试用例名称,感受到一种前所未有的清澈。大家几乎在几秒钟之内就做出了选择:这种形式是可以接受的,而且表达能力更强,交流效果不错。

什么? 使用中文作为函数名? 这似乎只是那些被主流舆论鄙视的"汉语编程"研究者才搞的东西,我们一直就被教育离这些东西远点,甚至汉语拼音都不推荐使用,一个经常拿来做反面教材的例子就是数据库表的列名使用汉语拼音,这被看作不专业的表现。又或者,以后团队中加入外国开发者怎么办?

幸运的是,我们是软件工程师,不是计算机科学家。学术理论可以极端,而工程一定是某种折衷。定理由自然界精确遵守,而工程却是各种应力的人为平衡。

具体到这个案例,让我们正视现实:

  1. 团队成员并不善长本项目领域的专业英语。
  2. 任何翻译都会造成一定的信息损失,尤其在一些具有中国特色的领域,比如"中专"翻译为英语就很难像中文一样简洁直观。
  3. 在可预见的将来,不会有老外加入开发团队。

而选用中文却能够让我们更好的坚持以下原则:

  1. 代码除了完成功能, 另外一个重要的功能是交流。(我们选择了对团队来说最有效的交流方式)
  2. 用测试用例的名字来描述需求。(用中文描述更精确, 易于理解)

当然我们也会失去一些东西,比如对上面提到的"应该坚持使用英文"原则的放弃。在这里我们认为放弃这条原则的收益大于损失。一种损失就是失去了学习英文的机会,比如上面最后一个测试用例,用中文写出来就是:

public void test_ 见习三副应该参加三副的培训 () {<br></br> ……<br></br>}或者有人会说:“见习"的英语是"intern”,常用词啊。然而系统中还有另外一类角色,叫"实习三副"等,那才是"intern"。实习是实际动手,担当实际的职责;见习是只看不练,跟在后面观摩学习。见习的英文单词是"noviciate",并不为项目组所熟悉,而我们也不再关心它。

总之,在实践中应当权衡各种利弊,选择对你来说最有效的方式。

2. 用大量测试来驱动

在项目组中曾经发生过自以为完成了某个特性,后来却发现漏掉了某些验收条件,甚至是比较重要验收条件的事情。而这也是 TDD 经常被质疑的一点,就是如何保证测试的完备性。因为总是想一点写一点,不经过深入的思考,不可避免的会漏掉某些测试条件。

然而,TDD 并不妨碍你深入思考,只是劝你在实现时步子不要太大,小步前进获得对问题的进一步认识以随时调整设计和实现。不过今天不争论 TDD 的哲学问题,我们只是关注用什么样的实践来消除对于 TDD 测试完备性的疑虑。

事实上,完全可以根据需求文档,验收条件,进行"深入思考",从一开始就写下所有能想到的单元测试用例,就跟测试人员在产品出来前就对着需求准备测试用例一样。

哦,等等,这还叫 TDD 吗? TDD 所强调的小步前进,随时应变哪里去了?为全面的测试用例所花费的大量努力岂不是有浪费的风险?

嗯,不错,TDD 强调小步前进的原因就是要避免浪费。如果我们能找到一种方法,既能够提醒我们不要忘记需求,又让我们在需求变化时不致浪费太多,岂不是皆大欢喜?

想想,我们用什么来描述需求? 是测试用例名称,而不是测试用例的函数体,而名称的书写几乎是没有成本的。从需求文档中把验收条件抠出来即可。如:

public void test_ 会被认为不服从调配 _if_the_seaman_ 在当前职位上曾经旷工 () {<br></br> // TODO<br></br> }<p> public void test_ 会被认为不服从调配 _if_the_seaman_ 在当前职位上曾经请过病假 () {</p><br></br> // TODO<br></br> }<p> public void test_ 会被认为不服从调配 _if_the_seaman_ 在当前职位上曾经请过事假 () {</p><br></br> // TODO<br></br> }<p> public void test_ 会被认为不服从调配 _if_the_seaman_ 在当前职位上曾经被遣返 () {</p><br></br> // TODO<br></br> }<p> public void test_ 不会被认为不服从调配 _if_the_seaman_ 在当前职位上从未旷工 _ 请病假 _ 请事假 _ 和遣返 () {</p><br></br> // TODO<br></br> }两分钟,我们就把这个用户故事的测试用例按照验收条件里说的全部描述出来了。函数体全部都是空的,因此所有的测试都是通过的,不会强迫你一次性把所有的测试都实现。

每次你流览或修改这段代码,空的函数体或里面的// TODO都会提醒你还有测试没有完成。即使你不在这个特性上工作了,切换过来的 Pair 也会从你遗留的测试用例名称中迅速的了解需要做什么。

这种方法是怎么解决问题的呢?

第一,还是让我们正视现实:如果需求描述不和代码放在一起,开发人员很少会在开发过程中去翻阅需求文档,甚至是特性编码结束后。这在成熟的开发团队中会有改善,但仍然不可避免。把需求描述以测试用例名称的方式放进代码,便会无时无刻不在提醒开发者,还有这个这个这个验收条件没满足。

第二,我们依然坚持了以下原则:

  1. 用测试用例的名字来描述需求。
  2. 小步前进,编写一个测试用例,实现一段产品代码,编写下一个测试用例,实现下一段产品代码。(因为所有的未完成测试都是通过的,不妨碍你运行测试,提交代码和持续集成)
  3. 当实现过程中发现事情并不是当初想的那样时,随时更改或删除之前写的测试用例,不会造成大的浪费。(因为只是函数名加空的函数体,成本很低)

在开始写下"所有"的测试用例名称并不意味着一劳永逸。中间当需求发生变化,我们需要对应的添加或删除一部分测试用例。在实现的过程中,发现某些条件不在验收范围内,或许是之前没考虑到,那么跟 BA/QA 确认后,需要添加到用例列表中。

(细心的读者可能发现,这里面存在着重复。就是以普通文本形式存在的验收条件和以测试用例名称存在的验收条件。或许应该有类似 SVN2Wiki 的工具,来消除这类重复)

当然,完备性还有其它的含义和检测条件,不是说你提前多写几个用例就是完备了。这里只是用一种成本最低的方式来解决问题。

3. 一个环境,多个断言

随着时间的推移,项目组发现测试运行的越来越慢。当然,这是一个普遍现象,也已经有很多方法来加速测试的运行速度。但很多需要新工具的支持,而项目组暂时没时间去切换工具。有没有其它更方便的做法?

像其它性能问题一样,我们首先需要确定瓶颈在哪里。我们发现主要是每个测试用例运行前搭建测试所需的环境相对较为耗时,尤其是 Selenium 测试,它需要启动和关闭浏览器。并且,很多测试用例其实使用相同的环境设置,只是每个用例仅仅去断言其中一个需求。

我们可以修改 Runner,让一组测试使用同一个浏览器实例,每次环境的清理通过清理 Session 来完成。而我们也可以采取另外一种方法,就是合并使用相同环境设置的测试用例,把它们的断言都放进同一个用例。

哦,这又违反了 Kent Beck 为 TDD 制定的原则:每个测试用例最好只有一个断言。

好,让我们再一次分析原则背后的理念。一个用例一个断言,是为了让测试更清晰,更精确的描述需求,测试失败更容易定位。那么有没有一种方法,既能让多个断言共享相同的环境设置,又能清晰精确的描述需求呢?

想想,我们用什么来描述需求? 是测试用例的名称,确切的说,是函数的名称。只要我们把一组组相关的断言封装到一个个函数里,给它们一个能够清晰精确的描述这组断言对应的需求的名称,然后在测试用例里面调用这些函数就可以了。这样我们只需为多个断言设置一次环境,而同时又保留了清晰精确的表达需求的能力。

@Test<br></br> public void test_should_show_step_details_info_in_todo_item_page() throws Exception {<br></br> TodoItemPage page = navigator.gotoTodoItemPage( );<p> should_show_step_name_as_page_title(activeStepOfNonStartedInstance, page);</p><br></br> should_show_start_processing_button_if_current_step_status_is_waiting(page);<br></br> should_show_transition_buttons(activeStepOfNonStartedInstance, page);<br></br> should_NOT_ask_user_to_input_his_opinion_if_current_step_status_is_NOT_processing(page);<br></br> should_show_comment_box_after_click_start_process(page);<br></br> }<p> private void should_show_step_name_as_page_title(FlowStep step, TodoItemPage page) {</p><br></br> assertEquals(step.getName(), page.title());<br></br> }<p> private void should_show_start_processing_button_if_current_step_status_is_waiting(TodoItemPage page) {</p><br></br> assertTrue(page.isStartProcessingButtonVisible());<br></br> }<p> private void should_show_comment_box_after_click_start_process(TodoItemPage page) {</p><br></br> page。clickStartProcessingButton();<br></br> assertTrue(page.isCommentBoxAppear());<br></br> }<p> private void should_ask_user_to_input_his_opinion_if_current_step_status_is_processing(TodoItemPage page) {</p><br></br> assertTrue(page.isCommentBoxVisible());<br></br> assertTrue(page.isActionButtonsVisible());<br></br> }<p> private void should_ask_user_to_select_next_step_operators(FlowTransitionDefinition nextTransitonOfStep,</p><br></br> TodoItemPage page) {<br></br> assertTrue(page。isUserGroupVisible(nextTransitonOfStep.getId()));<br></br> }<br></br> ……## 小结

回过头来我们看看上面的三个实践,它们如出一辙的,一次又一次的"违反"了某种原则。它们分别是"不能用汉语",“不能一次编写多个测试用例”,和"不能在一个用例里面使用多组断言",而实际上,我们违反的只是这些原则的外在形式,但却坚持了这些原则背后的思想,如最有效的沟通,注重实效而不是形式。以此为基石,我们可以在出现新的约束的情况下,灵活运用,发明各种实践,并享受由此带来的效率提升。


作者简介:李光磊,软件工程师,同时还是一位敏捷教练,就职于 ThoughtWorks。他还是活跃的 blog 作者,了解他最新的想法,请访问 http://blog.csdn.net/chelsea

2008-10-20 01:023759

评论

发布
暂无评论
发现更多内容

MobTech观察 | CSDN:企业数字化转型如何提升段位?杨冠军畅谈企业数字化前世今生

MobTech袤博科技

数据分析 数字化转型 数据治理 企业 数据可视化

喜讯!Apache APISIX Committer 张晋涛当选「中国开源先锋 33 人」

API7.ai 技术团队

云原生 微服务网关 APISIX 网关

零数科技入选毕马威中国“2021领先金融科技50企业”

科技热闻

知识中台,驱动产业智能化升级

百度大脑

人工智能

Go Error 嵌套到底是怎么实现的?

AlwaysBeta

Go 源码 源码阅读 Go 语言 源码学习

共建龙蜥社区,支撑商业建设

OpenAnolis小助手

Linux 开源 开发者

全面容器化之后,来电科技如何实现微服务治理?

阿里巴巴云原生

阿里云 微服务 云原生 实践

【伙伴故事】一盏智能灯,点亮家庭和工业照明的新未来

华为云开发者联盟

物联网 华为云 AIOT PLC 智能照明

面试官: Flink双流JOIN了解吗? 简单说说其实现原理

华为云开发者联盟

sql flink join 双流join 数据库SQL

MASA Framework - EventBus设计

MASA技术团队

C# .net 微软 框架 Framework

移动数字化平台如何让企业生态协同更高效?

BeeWorks

零数科技入选毕马威中国“2021领先金融科技50企业”

科技热闻

4种高速安全混合云解决方案,助力您的云迁移之旅!

亚马逊云科技 (Amazon Web Services)

计算

开源操作系统年度技术会议演讲PPT下载来啦!

鉴释

开源 操作系统

利用 Amazon Batch 来为容器化负载调用海量云端算力

亚马逊云科技 (Amazon Web Services)

计算

武汉智慧城市建设新名片 一城一云打造数字经济新引擎

InfoQ_967a83c6d0d7

“云联邦”构建连云成片、无缝混合的一朵云

华为云开发者联盟

混合云 多云 华为云Stack 云联邦 联邦认证

微帧ROI视频智能编码:基于人眼感兴趣区域,实现极致观感体验

微帧Visionular

视频编码

产业协同,助力数转 | 鲸鲮正式加入中国电信5G产业创新联盟

鲸鲮JingOS

Linux 5G 操作系统 信创 电信

通过 Amazon CloudWatch 配合 Amazon ElastiCache for Redis 遵循监控最佳实践

亚马逊云科技 (Amazon Web Services)

计算

做网络安全竟然不了解ATT&CK?这篇文章的介绍详细到令人尖叫

博文视点Broadview

详解数据湖:概念、特征与架构

五分钟学大数据

数据湖 1月月更

Hoo虎符研究院 | 币圈后浪——Osmosis一种高级AMM协议

区块链前沿News

Hoo 虎符交易所 虎符研究院

构建面向异构算力的边缘计算云平台

火山引擎边缘云

gpu 云原生 边缘计算 算力

【Node Weekly #417】你需要了解的Node.js内存限制

道道里

前端 Node

中小型企业过等保困难有哪些?如何解决?

行云管家

网络安全 企业 过等保

关于减碳你是否有很多问号?施家碳中和咨询服务来了!

ToB行业头条

【等保小知识】等级保护工作是指等保测评吗?意思一样吗?

行云管家

网络安全 等保 等保测评

4种Spring Boot 实现通用 Auth 认证方式

华为云开发者联盟

拦截器 spring-boot Auth 传统AOP 参数解析器

🍃【Spring专题】「技术原理」为大家介绍一下Spring中的Ant路径匹配工具组件AntPathMatcher

码界西柚

spring 1月月更 SpringFramework AntPathMatcher

快过年了,用五种不同的JS特效带你看烟花

海拥(haiyong.site)

大前端 js 28天写作 前端特效 1月月更

TDD实践之实用主义_研发效能_李光磊_InfoQ精选文章