用 Safari 11+、Firefox 70+ 浏览效果最佳
我的文章 我的评论 我的书评 我的知乎*
书单 书讯 书评
需求知识体系 特性 用例 统一用例方法 用户故事 需求工具
业务建模 UML OOD
敏捷知识体系 敏捷方法 敏捷问答 敏捷工具 敏捷评论 敏捷资源
业务模式 需求模式 架构模式 设计模式 大道至简:实话设计模式 Web 应用架构模式
.NET Java JS 笔记
Amazon* ITPub* Martin Fowler* 教程
需求分析需求模型非功能需求业务需求分析
SpringJSF
> >
在线/2 登录/0

案例:TDD 实践之实用主义

基本

李光磊(ThoughtWorks 公司软件工程师、敏捷教练)发表在 InfoQ China 上的文章《TDD 实践之实用主义》。文章的主要部分为 3 个小节: 1、为沟通选择语言; 2、用大量测试来驱动; 3、一个环境、多个断言。 我基本上赞同作者所提倡的实用主义和创新观点,不要拘泥于各种教条,但作者所提出的几个“创新”解决方案和理由却不能让我满意。在原文中,我们看到了不少违反常规的代码,尤其是那些冗长的、中文的、包含 if 条件的测试方法名。 一开始,作者的同事们是这样编写测试方法的: [ref]
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() { ... } public void test_should_return_third_mate_course_for_jianxi_third_mate() { ... }
[/ref] 读者朋友,当您第一眼看到以上代码时有什么感觉?我想您的感受可能跟我的、作者的一样,yes, it's terribly a mess,一团糟。 这种代码的可读性太差了,于是,作者建议把以上代码重构为: [ref]
public void test_见习三副应该参加三副的培训() { ... } public void test_应该算未通过_if_职务高于二副二管轮_而_学历只是中专_并且_毕业时间晚于2002年2月1日() { ... }
[/ref] 那么,作者重构之后的代码与之前的代码有何区别?显而易见,最大的差别在于一个是中文,一个是英文,而内容上其实没有差别。为了大家学习,张恂特地将作者的这种重构手法命名为: Refactoring: Changing English method names into Chinese method names. 以及 Refactoring: Changing Chinese method names into English method names. 在以上代码中,我们看到作者及其同事们把冗长的测试结果和测试条件的说明用作方法名,并为此制定了相应的命名规范(should_return_NOT_pass、should_return、“应该算未通过”等等),这么做的理由是什么?除了进行了英译中之外,作者没有给出明确的解释。 这么做有哪些优、缺点?说实在话,这种命名方式,张恂在过去 20 多年的专业及业余编程经历中(从小学 5 年级算起)还从未遇到过,也从未使用过(除了一些无需人工照看的自动生成代码),它们与本人的认知水平相距实在甚远。头一眼看到这种代码,我的直觉告诉我,我肯定不会采用,因为这样的方法名太长、太复杂,难以阅读和理解,而且难以键盘输入 ... 为什么 TDD 了,敏捷了,我们就不能采用更简单明了的、便于维护的测试方法名了呢?难道编写测试程序,除此以外,就没有更好的方法了吗? 所以,我觉得自动测试的方法名究竟应该怎么写,采用哪种格式更好,是不是应该采用超长的、包含测试条件说明的方法名,是研究本案的一个核心问题。带着疑问,在以下小节中,我对这一现象进行了进一步的分析和讨论: [section=infoqtddpractice,smellif]Bad Smell: 把测试条件、测试说明用作测试方法名[/section] 后续其他的例子还包括: [ref]
public void test_会被认为不服从调配_if_the_seaman_在当前职位上曾经旷工() { // TODO } public void test_会被认为不服从调配_if_the_seaman_在当前职位上曾经请过病假() { // TODO } public void test_会被认为不服从调配_if_the_seaman_在当前职位上曾经请过事假() { // TODO } public void test_会被认为不服从调配_if_the_seaman_在当前职位上曾经被遣返() { // TODO } public void test_不会被认为不服从调配_if_the_seaman_在当前职位上从未旷工_请病假_请事假_和遣返() { // TODO } @Test public void test_should_show_step_details_info_in_todo_item_page() throws Exception { ... } private void should_show_step_name_as_page_title(FlowStep step, TodoItemPage page) { ... } private void should_show_start_processing_button_if_current_step_status_is_waiting(TodoItemPage page) { ... } private void should_show_comment_box_after_click_start_process(TodoItemPage page) { ... } private void should_ask_user_to_input_his_opinion_if_current_step_status_is_processing(TodoItemPage page) { ... }
[/ref] 在文末,作者总结道: [ref]回过头来我们看看上面的三个实践,它们如出一辙的,一次又一次的"违反"了某种原则。它们分别是"不能用汉语","不能一次编写多个测试用例",和"不能在一个用例里面使用多组断言",而实际上,我们违反的只是这些原则的外在形式,但却坚持了这些原则背后的思想,如最有效的沟通,注重实效而不是形式。以此为基石,我们可以在出现新的约束的情况下,灵活运用,发明各种实践,并享受由此带来的效率提升。 [/ref] 总体上,我肯定作者反对教条、提倡创新和重构的思路,这种精神值得发扬。但另一方面,我认为作者的重构在技术上还没有到位,为了实现“注重实效而不是形式”的目标,我们还需要继续重构。 原文的篇幅不长。从目前作者所提供的信息来看,我认为作者的分析是比较片面的,因为过分自信而导致片面,只看到了这些“创新”做法可能带来的好处和优点,而对这些做法存在哪些缺点和不足,却分析得不够,既缺乏深入性和全面性,也缺乏前瞻性。作者及其同事们所给出的三个实践,尤其是直接把冗长的有关测试/验收条件、测试结果的说明用作(简单地拷贝为)测试用例的方法名这一做法,违反了简化、稳定、灵活和 DRY(Don't Repeat Yourself)等敏捷软件的基本设计原则和思想。我认为目前原文中给出的解决方案,只能算是一些低成本的、脆弱的、不稳定的临时措施(临时搭建的工棚?),存在明显的 bad smells,需要通过重构来进一步提高测试程序的质量。 当然,如果作者或他人能够给出支持这些做法的更充分的信息和理由,我也准备接受。 以下是我的详细分析、评论和建议。
本文采用复杂的测试条件作为方法名,降低了测试程序的可读性、可维护性,容易产生错误。建议不要采用。

关于为什么要这么做,作者写道:

Team里的人纷纷围过来,看着这个跟需求描述里的验收条件几乎一模一样的测试用例名称,感受到一种前所未有的清澈。大家几乎在几秒钟之内就做出了选择:这种形式是可以接受的,而且表达能力更强,交流效果不错。

...

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

...

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

两分钟,我们就把这个用户故事的测试用例按照验收条件里说的全部描述出来了。函数体全部都是空的,因此所有的测试都是通过的,不会强迫你一次性把所有的测试都实现。

...

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

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

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

以下面这个测例为例:
public void test_应该算未通过_if_职务高于二副二管轮_而_学历只是中专_并且_毕业时间晚于2002年2月1日() { ... }

“应该算未通过_if_职务高于二副二管轮_而_学历只是中专_并且_毕业时间晚于2002年2月1日”,难道这就是“测试用例的名称”?

我们看到,这个短语中包含了三个测试变量及其条件(职务、学历和毕业时间),以及测试结果(“应该算未通过”)。

这种写法还有一个明显的错误:这个测例究竟测试的是什么呢?测试的目标对象是什么?我们只看到了职务、学历和毕业时间的取值,以及“应该算未通过”,但究竟是“什么未通过”呢?缺乏主语。我们不知道完整的输出结果,测试的结果状态涉及到哪些变量,有哪些数据、信息或状态会发生变化。所以,即便是这样的测试说明(作者以为的测试用例的名称)也是不完整的。

这种做法的一个显著特点是无法 scale up。如果一个测例涉及到的变量更多,条件更为复杂,导致测试说明短语的长度超过了 500 个字符,你还打算把它作为测试方法的名称吗?事实上,我们根本无法控制测试说明、测试条件和测试结果描述的长度,这与问题本身的复杂度有关。

Tip:Name and Description (or Note) are two things.

可见,作者及其同事们、Team 在这里犯了一个概念性的错误,把测试用例的名称与其详细说明混为一谈。 如何区分测试用例的名称与含有测试条件、测试结果的测试说明,其实很简单。以作者为例:

作者的名称(姓名)是:李光磊

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

test case 方法名其实可以很简单,比方用某些关键词结合 ID 来命名。说明测试的目标或内容,为什么不能采用注释、注解、映射表或其他辅助工具呢?

把冗长的测试条件、测试结果说明用作测试方法名称,有不好的效果,可以说是一种 bad smell。

Bad Smell: Long, complex test conditions as method name


采用了冗长的方法名后,必然会降低程序的可读性,很容易引起程序的错误。
原先的代码:

@Test 
public void test_should_show_step_details_info_in_todo_item_page() throws Exception { 
	TodoItemPage page = navigator.gotoTodoItemPage( ); 
	should_show_step_name_as_page_title(activeStepOfNonStartedInstance, page); 
	should_show_start_processing_button_if_current_step_status_is_waiting(page); 
	should_show_transition_buttons(activeStepOfNonStartedInstance, page);	
	should_NOT_ask_user_to_input_his_opinion_if_current_step_status_is_NOT_processing(page); 
	should_show_comment_box_after_click_start_process(page); 
} 

private void should_show_step_name_as_page_title(FlowStep step, TodoItemPage page) { 
	assertEquals(step.getName(), page.title()); 
} 

private void should_show_start_processing_button_if_current_step_status_is_waiting(TodoItemPage page) {
	assertTrue(page.isStartProcessingButtonVisible()); 
} 

private void should_show_comment_box_after_click_start_process(TodoItemPage page) { 
	page.clickStartProcessingButton(); 
	assertTrue(page.isCommentBoxAppear()); 
} 

// ZX: 与上面的 should_NOT 不一致。
private void should_ask_user_to_input_his_opinion_if_current_step_status_is_processing(TodoItemPage page) { 

	// ZX: 如果是 should_NOT,这里应该用 assertFalse。
	assertTrue(page.isCommentBoxVisible()); 
	assertTrue(page.isActionButtonsVisible()); 
} 

// ZX: 作者遗忘了提供 should_show_transition_buttons 的代码。

以上程序不但可读性差,而且好像还存在着逻辑错误。

对原文代码重构后的结果


原先代码中的几个 private 方法,完全可以采用更加简洁而有效的名称。

Class TestTodoItemPage

@Test
public void testStepDetailsInfoScenarioA() throws Exception {
	TodoItemPage page = navigator.gotoTodoItemPage(); 

	verifyPageTitle(activeStepOfNonStartedInstance, page);

	verifyStartProcessingButton(page);

	verifyTransitionButtons(activeStepOfNonStartedInstance, page);

	verifyNotAskUsrInputOpinion(page);

	page.clickStartProcessingButton(); 
	verifyCommentBox(page);
}


// = step name
private void verifyPageTitle(FlowStep step, TodoItemPage page) { 
	assertEquals(step.getName(), page.title()); 
} 

// if step status is waiting
private void verifyStartProcessingButton(TodoItemPage page) {
	assertTrue(page.isStartProcessingButtonVisible()); 
} 

// if step status is NOT processing
private void verifyNotAskUsrInputOpinion(TodoItemPage page) { 
	assertFalse(page.isCommentBoxVisible()); 
	assertFalse(page.isActionButtonsVisible()); 
} 

// after clicking start processing
private void verifyCommentBox(TodoItemPage page) { 
	assertTrue(page.isCommentBoxAppear()); //?? page.isCommentBoxVisible()
} 
[h2new]Bad smell: 冗长的方法名,而且还是中文的[/h2new] 赞成作者的实用主义观点。但冗长的方法名,而且还是中文的,实在有点离谱。 目前的这个方案是脆弱的,难以适应变化(中文方法名称本身就是不稳定的),我觉得作者给出的理由也是不充分的。中文术语名称,也未必比英文术语更稳定、更准确吧。 这样就真的做到了“最有效的沟通”和“最有效的交流方式”?而用了这么多下划线,是自找麻烦,可读性反而下降了。 关于为什么采用中文,作者解释道: [ref]具体到这个案例,让我们正视现实: 1. 团队成员并不善长本项目领域的专业英语。 2. 任何翻译都会造成一定的信息损失,尤其在一些具有中国特色的领域,比如"中专"翻译为英语就很难像中文一样简洁直观。 3. 在可预见的将来,不会有老外加入开发团队。 而选用中文却能够让我们更好的坚持以下原则: 1. 代码除了完成功能, 另外一个重要的功能是交流。(我们选择了对团队来说最有效的交流方式) 2. 用测试用例的名字来描述需求。(用中文描述更精确, 易于理解) 当然我们也会失去一些东西,比如对上面提到的"应该坚持使用英文"原则的放弃。在这里我们认为放弃这条原则的收益大于损失。一种损失就是失去了学习英文的机会,... [/ref] 总之,用中文方法名,尤其还是冗长的、难以输入、维护和适应变化的方法名,是个 bad idea,我一开始就不会用(省却了重构的麻烦)。主要原因是因为有生以来,张恂没有任何的中文编程经验。作者的文章倒提醒了我,既然像 Java 这样的先进编程语言现在都支持 Unicode 了,从今以后我们是否可以进入汉语编程时代? 当然,我也不完全反对中文/汉语编程,尤其是在有特殊原因,而且采用了中文编程没有什么不良后果或后遗症的情况下。关于采用何种语言进行编程(如类名、方法名、属性名等标识符),我推荐以下顺序: 1、英文 2、汉语拼音 3、中文 [h2new]重构方法名:test_见习三副应该参加三副的培训[/h2new] “test_见习三副应该参加三副的培训”怎么看都不像一个合理的方法名,建议避免使用。 [ref]
比如上面最后一个测试用例,用中文写出来就是: public void test_见习三副应该参加三副的培训() { ... }
[/ref] 太极敏捷最佳实践: 1)开发中所有人员一律采用统一的术语表和中英文译法; 2)对于常用的关键词、领域概念、专业术语,规定统一的英文简写、缩写和/或标记代码,以便在代码、文档、模型等各种工件中使用; Solution: 1)所有的专用术语“三副”均应采用缩写,比方 3M,见习三副(3Mn),实习三副(2Mi); 2)把所有与“见习三副”有关的测试都放入一个测试类中(基于高内聚原则),这样就可以去掉所有测试方法名前、累赘的“见习三副”; 举例: // TCNote: 见习三副应该参加三副的培训(该注释其实可省略) public void testAttend3MTraining() {...} 是不是更加简明、方便而有效?

<帮助> <全部评论> 共 1 个主题 1 条评论 (InfoQTDDPractice)
(1) 什么是 Acceptance Testing?
(张恂 223 字 0 回复 E2008-11-10 22:21:03 LID:1)
首页 | 使用指南 | 站点地图 | 版权声明 | 联系方法 | © 2005-2020 张恂 版权所有. 沪ICP备15017521号-2