2009年9月3日星期四
2009年4月26日星期日
跟踪团队的技术债
在开发的时候你碰到了一段散发着恶臭的代码,“这段重构有点棘手,要想把这段代码整理干净至少要花半天时间,打个patch的话马上就能解决,手里的故事不能再拖了,但是这段代码也不能放过。”,和Pair商量后,你们创建了一个Dev Card,在上面清清楚楚的写下了这段代码问题所在和大致的重构步骤。然后飞快的完成了故事。
几天以后,你对项目经理说有一段代码需要彻底清理,项目经理皱着眉头看了看计划:
”现在它还能工作,不是么?“
”是,可是...“
"这边还有两个bug,优先级很高,客户已经抱怨了。高优先级的必须首先完成,等我们有时间的时候再清理它吧,怎么样?"
"嗯,好吧..."
这段代码于是继续留在代码库里。这样的故事每天都发生着,”有时间“自然是从未发生过,有一件事开始折磨项目经理:不知道从什么时候开始团队开发速度变慢了,项目计划变的更紧了。
这些遗留下的Dev Card, 就是一笔笔技术债,和其他的债务一样,团队需要为此支付”利息“:这些未完成的技术任务会让添加功能、修复缺陷变得越来越困难,团队的开发速度也因此不断下降。
开发人员总在抱怨没有足够的时间来让代码闪闪发光,为什么Dev Card不能像像Story Card一样受到重视呢? 关键是Story Card姓$,每个人凭直觉便能认识到它们的价值。而Dev Card的价值一方面不是真金白银那般来得直接,另一方面只有专业的开发人员才能意识到它们的价值,然而对于$的迟钝却阻碍了开发人员认识到Dev Card究竟所值几何,在和项目经理、客户的谈判中如何能不败落?
和Story Card一样,如果我们对于Dev Card做出合理的估算,就可以在使用burn down图跟踪Story的同时,使用burn up图来跟踪每个迭代所增加的Dev Card的点数,结合开发速度的下降,所有人都可以直观的认识到Dev Card的价值,进而帮助项目经理和客户做出更合理的,符合可持续发展的计划。
那么对于Dev Card的大小不清楚怎么办? Spike,和Story一样,需要对技术问题进行摸索,做到对工作量心中有数,这样的Spike,一方面可以帮助团队找到最佳的解决方案,另一方面可以使用Dev Card的估算值与开发经理、客户share understanding.
与Story Card的处理原则一样,如果一张Dev Card过大,开发者需要将其变为若干较小,便于估算的Dev Card,从而使团队有能力小步的完成这些技术任务。
这些想法是上周的回顾会议中得到的,也将在项目进行实践,如果可以的话,希望能把项目中的一些数据发布出来,相信会非常有趣。
几天以后,你对项目经理说有一段代码需要彻底清理,项目经理皱着眉头看了看计划:
”现在它还能工作,不是么?“
”是,可是...“
"这边还有两个bug,优先级很高,客户已经抱怨了。高优先级的必须首先完成,等我们有时间的时候再清理它吧,怎么样?"
"嗯,好吧..."
这段代码于是继续留在代码库里。这样的故事每天都发生着,”有时间“自然是从未发生过,有一件事开始折磨项目经理:不知道从什么时候开始团队开发速度变慢了,项目计划变的更紧了。
这些遗留下的Dev Card, 就是一笔笔技术债,和其他的债务一样,团队需要为此支付”利息“:这些未完成的技术任务会让添加功能、修复缺陷变得越来越困难,团队的开发速度也因此不断下降。
开发人员总在抱怨没有足够的时间来让代码闪闪发光,为什么Dev Card不能像像Story Card一样受到重视呢? 关键是Story Card姓$,每个人凭直觉便能认识到它们的价值。而Dev Card的价值一方面不是真金白银那般来得直接,另一方面只有专业的开发人员才能意识到它们的价值,然而对于$的迟钝却阻碍了开发人员认识到Dev Card究竟所值几何,在和项目经理、客户的谈判中如何能不败落?
和Story Card一样,如果我们对于Dev Card做出合理的估算,就可以在使用burn down图跟踪Story的同时,使用burn up图来跟踪每个迭代所增加的Dev Card的点数,结合开发速度的下降,所有人都可以直观的认识到Dev Card的价值,进而帮助项目经理和客户做出更合理的,符合可持续发展的计划。
那么对于Dev Card的大小不清楚怎么办? Spike,和Story一样,需要对技术问题进行摸索,做到对工作量心中有数,这样的Spike,一方面可以帮助团队找到最佳的解决方案,另一方面可以使用Dev Card的估算值与开发经理、客户share understanding.
与Story Card的处理原则一样,如果一张Dev Card过大,开发者需要将其变为若干较小,便于估算的Dev Card,从而使团队有能力小步的完成这些技术任务。
这些想法是上周的回顾会议中得到的,也将在项目进行实践,如果可以的话,希望能把项目中的一些数据发布出来,相信会非常有趣。
2009年3月18日星期三
为什么我们要放弃Subversion
有日子没更新blog,这段时间忙着准备发布junit-ext
1.0,另外就是写InfoQ的两篇约稿,第一篇《为什么我们要放弃Subversion》已经发了。请大家不吝指教,对DVCS感兴趣的同学也可来电来函一起讨论。
1.0,另外就是写InfoQ的两篇约稿,第一篇《为什么我们要放弃Subversion》已经发了。请大家不吝指教,对DVCS感兴趣的同学也可来电来函一起讨论。
2008年12月28日星期日
功能测试之美
功能测试之乐:
功能测试定义了产品的业务需求,通过它业务人员可以了解系统是否能在各个业务场景下正常工作。功能测试通常使用某种自动化测试框架编写,这样开发者可以从自动化的功能测试中获得快速反馈,为下阶段新功能的开发或软件内部实现的重构提供帮助。另一方面,它大大减少了手动环节可能引入的错误,而将枯燥的回归测试交给机器完成,在加快测试速度的同时,将质量保证人员解放出来,从而使他们可以更多地关注于创造性的探索测试。通过从用户角度进行的功能测试,团队对系统在真实条件下的可用性充满信心,而自动化的功能测试也大大提高了工作效率。这样一来,产品能以更高的质量,更快的速度进入市场。
功能测试之苦:
* 保持验收条件及其技术实现的同步
在任何开展自动化功能测试的敏捷开发团队中通常会存在两套系统、 一套是BA(业务分析师)\QA(质量保证人员)所编写、维护的验收条件,可能以纸卡、Wiki等形式记录下来。 另一套是验收条件的技术实现。以源代码的形式记录,由开发者维护。两套系统的并行发展, 带来了同步的问题。如何快速将验收条件转变为技术实现?当验收条件变化时如何使源代码部分同步变化?可不可以将验收条件与测试关联起来,利用Rspec的思路消除这个并行的系统?
* 无法系统化的划分测试
速度是阻碍频繁运行功能测试的主要原因。进行功能测试的团队常常花费数十分钟甚至数小时来运行完整的功能测试套件,加快测试几乎总以失败而告终,从用户角度进行业务场景的测试决定了功能测试的速度天生就是缓慢的。随着软件功能的日益完善,更多的测试被添加到套件中,庞大的套件也使得测试运行的时间越来越长。无法快速得到反馈使团队没有安全感,同时大大减缓开发的脚步,烦躁的开发者甚至开始逃避运行测试,将不安全的代码集成到产品中。
只运行相关的测试,听起来这似乎是一个解决方案。当开发者修改了登录模块的实现后,为什么他们非得花费1个小时等待其他模块的测试结果呢?如果可以仅仅运行登录模块相关的测试,将其余的测试留给持续集成工具运行,开发的效率将大大提高。但遗憾的是在XUnit的世界中,系统化的划分测试并不是件容易的事情。 不论是利用文件名和目录来区分,还是手工维护测试套件,最终总会变成难以维护的大泥球。
* 阅读测试花费的大量时间
大量测试代码总是难以阅读。随着项目的进行, 各种不同习惯的缩写出现在代码中、 测试代码中出现的大量的方法、设置数据越来越复杂等都给阅读测试带来了极大的麻烦。面对失败的测试,试图修复它的开发者总是需要在复杂的代码中挣扎着找出测试的意图,过滤掉准备数据的过程,抽丝剥茧地找到这个测试所覆盖的业务流程,分析究竟是哪个产品模块出了问题。出现这样问题的根源是没有对测试的目的(业务价值)和技术实现做出合理的抽象。
能否有这样一个视图,过滤掉所有让人分散注意力的方法(私有方法,准备数据、清理数据的方法等)让开发者可以清楚地看到测试的目的和步骤,甚至让测试以一种自然语言的形式展现出来,让非技术人员也可以轻易地阅读修改测试呢?
* 启动功能测试的花费
在任何一个团队开始功能测试的第一步,总是耗时和痛苦的。下载各种依赖的库文件,在脚本中进行配置, 保证功能测试可以在命令行执行, 同时还要对IDE进行配置,让开发者可以从IDE中方便地运行单个测试。
能不能缩短这个过程,减轻开发者的痛苦?
功能测试工具的趋势:
敏捷联盟在2007年10月召开了专题讨论来展望下一代的功能测试工具。在这次讨论中, 功能测试领域的专家们提出了对下一代功能测试工具的展望:
* 需要一个整合的开发环境帮助团队:重构测试元素、自动完成命令、增量式的语法检验(基于测试领域特定语言)、快捷键支持、调试等等
* 需要更具描述性的测试领域语言,如将可执行文件、文字、表格、图片、颜色整合到一个测试用例中
* 需要特定的测试领域语言使测试更具阅读性并容易维护
* 需要具备可以使用多种方式查看/导航测试的能力,来帮助我们了解某个部分与整个领域或者特性之间的联系;将测试按照领域上下文来组织;按照用户定义的关键字进行搜索(跨横切关注点)
Twist
经历过很多的功能测试之苦,我们团队尝试了使用Twist来编写、管理功能测试,并得到了很好的效果。它是ThoughtWorks Studio为软件团队设计开发的下一代协作功能测试平台,提供了一个编写、执行以及维护测试的丰富环境。Twist以Eclipse插件的形式设计并开发,充分利用了Eclipse强大的编辑功能, 支持测试专家们通过DSL来表达测试意图。通过自动完成功能,开发人员可以利用Eclipse中广为人知的CTRL + 1 (帮助完成)来快速地将验收条件转换为相应的技术实现(测试类和方法)。由于Twist中的Driver是以Spring Bean的形式注入到测试中,开发者可以以Twist作为平台,使用任何熟悉的DRIVER(如Selenium、Watir、fraiksen)来编写测试。
那么Twist是怎样帮助团队来减轻测试之苦的呢?
* 保持验收条件以及其技术实现的同步
之前我们将验收条件保存在Mingle中的故事卡里,而现在QA和BA通过Twist来编写验收条件,并标记为in-progress。在实现的时候,开发者通过IDE来自动生成测试。在任何人修改验收条件或者技术实现时,IDE的重构功能将自动完成它们之间的同步。例如:测试专家们在Twist IDE 中可能写出如下的验收条件:
开发者通过TWIST IDE, 可以生成如下测试代码(以Java为例)
之后开发人员可以选择通过手工编写代码(例如通过使用Selenium、 watir作为Driver )或者录制的方式(Twist会记录鼠标、键盘在WEB页面上的动作并转变为相应的测试脚本)来实现功能测试。 更方便的是, 测试专家们编写的DSL验收条件在保持可读性的同时,可以像代码一样自动完成、对重构更加友好、并且可以方便的运行。
在开发者对相应的源代码进行诸如重命名或者引入参数(Introduce Parameter)等重构操作时,相应的测试DSL也会被改变.
* 对测试进行标签管理
Twist使用标签(Tag)对测试进行管理,开发团队可以使用任意标签来标注测试。例如:已完成还是未完成测试、对应的故事卡号、相应或者相关的模块名、属于回归测试还是冒烟测试。这样团队可以方便地划分测试。通过使用Twist发布的ant target中的tag属性,你可以轻松地分组运行测试。比如在我们的项目中最广泛使用的一个Tag是in-progress,这样,QA、BA可以随时提交他们的验收条件,这些尚未完成的验收条件(没有相应的技术实现)会被自动过滤掉,不会引起测试的失败。
*更有效率地 阅读测试
Twist很好地在产品中抽象了验收条件和技术实现,并将它们巧妙地关联起来。这样开发者可以通过易于阅读的DSL来快速了解上下文,并通过Eclipse的快捷键F3,快速地在验收条件和技术实现中进行切换,从而更有效率地阅读测试。
* 减少启动功能测试的花费
用户可以通过在Eclipse中创建Twist项目,快速展开测试。 Twist项目包含了运行Twist测试所必需的所有的第三方库。同时,Twist发布了相应的ANT target,大大减少了团队用于启动功能测试的时间。
在使用Twist的过程中,我们也发现了一些问题,主要集中在Twist的IDE不够稳定,会有一些UI的异常等方面。我们已经将BUG提交到了Twist团队的论坛里,相信稍后的版本会更加稳定易用。 通过使用Twist,很好地将团队以功能测试为中心整合在一起,团队中的所有角色可以通过一套IDE来编写、实现、运行、维护测试,大大减少了交流成本,提高了开发的速度,体会了开发之乐。
功能测试定义了产品的业务需求,通过它业务人员可以了解系统是否能在各个业务场景下正常工作。功能测试通常使用某种自动化测试框架编写,这样开发者可以从自动化的功能测试中获得快速反馈,为下阶段新功能的开发或软件内部实现的重构提供帮助。另一方面,它大大减少了手动环节可能引入的错误,而将枯燥的回归测试交给机器完成,在加快测试速度的同时,将质量保证人员解放出来,从而使他们可以更多地关注于创造性的探索测试。通过从用户角度进行的功能测试,团队对系统在真实条件下的可用性充满信心,而自动化的功能测试也大大提高了工作效率。这样一来,产品能以更高的质量,更快的速度进入市场。
功能测试之苦:
* 保持验收条件及其技术实现的同步
在任何开展自动化功能测试的敏捷开发团队中通常会存在两套系统、 一套是BA(业务分析师)\QA(质量保证人员)所编写、维护的验收条件,可能以纸卡、Wiki等形式记录下来。 另一套是验收条件的技术实现。以源代码的形式记录,由开发者维护。两套系统的并行发展, 带来了同步的问题。如何快速将验收条件转变为技术实现?当验收条件变化时如何使源代码部分同步变化?可不可以将验收条件与测试关联起来,利用Rspec的思路消除这个并行的系统?
* 无法系统化的划分测试
速度是阻碍频繁运行功能测试的主要原因。进行功能测试的团队常常花费数十分钟甚至数小时来运行完整的功能测试套件,加快测试几乎总以失败而告终,从用户角度进行业务场景的测试决定了功能测试的速度天生就是缓慢的。随着软件功能的日益完善,更多的测试被添加到套件中,庞大的套件也使得测试运行的时间越来越长。无法快速得到反馈使团队没有安全感,同时大大减缓开发的脚步,烦躁的开发者甚至开始逃避运行测试,将不安全的代码集成到产品中。
只运行相关的测试,听起来这似乎是一个解决方案。当开发者修改了登录模块的实现后,为什么他们非得花费1个小时等待其他模块的测试结果呢?如果可以仅仅运行登录模块相关的测试,将其余的测试留给持续集成工具运行,开发的效率将大大提高。但遗憾的是在XUnit的世界中,系统化的划分测试并不是件容易的事情。 不论是利用文件名和目录来区分,还是手工维护测试套件,最终总会变成难以维护的大泥球。
* 阅读测试花费的大量时间
大量测试代码总是难以阅读。随着项目的进行, 各种不同习惯的缩写出现在代码中、 测试代码中出现的大量的方法、设置数据越来越复杂等都给阅读测试带来了极大的麻烦。面对失败的测试,试图修复它的开发者总是需要在复杂的代码中挣扎着找出测试的意图,过滤掉准备数据的过程,抽丝剥茧地找到这个测试所覆盖的业务流程,分析究竟是哪个产品模块出了问题。出现这样问题的根源是没有对测试的目的(业务价值)和技术实现做出合理的抽象。
能否有这样一个视图,过滤掉所有让人分散注意力的方法(私有方法,准备数据、清理数据的方法等)让开发者可以清楚地看到测试的目的和步骤,甚至让测试以一种自然语言的形式展现出来,让非技术人员也可以轻易地阅读修改测试呢?
* 启动功能测试的花费
在任何一个团队开始功能测试的第一步,总是耗时和痛苦的。下载各种依赖的库文件,在脚本中进行配置, 保证功能测试可以在命令行执行, 同时还要对IDE进行配置,让开发者可以从IDE中方便地运行单个测试。
能不能缩短这个过程,减轻开发者的痛苦?
功能测试工具的趋势:
敏捷联盟在2007年10月召开了专题讨论来展望下一代的功能测试工具。在这次讨论中, 功能测试领域的专家们提出了对下一代功能测试工具的展望:
* 需要一个整合的开发环境帮助团队:重构测试元素、自动完成命令、增量式的语法检验(基于测试领域特定语言)、快捷键支持、调试等等
* 需要更具描述性的测试领域语言,如将可执行文件、文字、表格、图片、颜色整合到一个测试用例中
* 需要特定的测试领域语言使测试更具阅读性并容易维护
* 需要具备可以使用多种方式查看/导航测试的能力,来帮助我们了解某个部分与整个领域或者特性之间的联系;将测试按照领域上下文来组织;按照用户定义的关键字进行搜索(跨横切关注点)
Twist
经历过很多的功能测试之苦,我们团队尝试了使用Twist来编写、管理功能测试,并得到了很好的效果。它是ThoughtWorks Studio为软件团队设计开发的下一代协作功能测试平台,提供了一个编写、执行以及维护测试的丰富环境。Twist以Eclipse插件的形式设计并开发,充分利用了Eclipse强大的编辑功能, 支持测试专家们通过DSL来表达测试意图。通过自动完成功能,开发人员可以利用Eclipse中广为人知的CTRL + 1 (帮助完成)来快速地将验收条件转换为相应的技术实现(测试类和方法)。由于Twist中的Driver是以Spring Bean的形式注入到测试中,开发者可以以Twist作为平台,使用任何熟悉的DRIVER(如Selenium、Watir、fraiksen)来编写测试。
那么Twist是怎样帮助团队来减轻测试之苦的呢?
* 保持验收条件以及其技术实现的同步
之前我们将验收条件保存在Mingle中的故事卡里,而现在QA和BA通过Twist来编写验收条件,并标记为in-progress。在实现的时候,开发者通过IDE来自动生成测试。在任何人修改验收条件或者技术实现时,IDE的重构功能将自动完成它们之间的同步。例如:测试专家们在Twist IDE 中可能写出如下的验收条件:
Search and buy book :
# search for book written by "Martin Fowler"
# add book "refactoring" to shopping cart
# check book "refactoring" is in my shopping cart
开发者通过TWIST IDE, 可以生成如下测试代码(以Java为例)
public SearchAndBuyBook {
public void searchForBookWrittenBy(String name) {
}
public void addBookToShoppingCart(String name) {
}
public void checkBookIsInMyShoppingCart(String name) {
}
}
之后开发人员可以选择通过手工编写代码(例如通过使用Selenium、 watir作为Driver )或者录制的方式(Twist会记录鼠标、键盘在WEB页面上的动作并转变为相应的测试脚本)来实现功能测试。 更方便的是, 测试专家们编写的DSL验收条件在保持可读性的同时,可以像代码一样自动完成、对重构更加友好、并且可以方便的运行。
在开发者对相应的源代码进行诸如重命名或者引入参数(Introduce Parameter)等重构操作时,相应的测试DSL也会被改变.
* 对测试进行标签管理
Twist使用标签(Tag)对测试进行管理,开发团队可以使用任意标签来标注测试。例如:已完成还是未完成测试、对应的故事卡号、相应或者相关的模块名、属于回归测试还是冒烟测试。这样团队可以方便地划分测试。通过使用Twist发布的ant target中的tag属性,你可以轻松地分组运行测试。比如在我们的项目中最广泛使用的一个Tag是in-progress,这样,QA、BA可以随时提交他们的验收条件,这些尚未完成的验收条件(没有相应的技术实现)会被自动过滤掉,不会引起测试的失败。
*更有效率地 阅读测试
Twist很好地在产品中抽象了验收条件和技术实现,并将它们巧妙地关联起来。这样开发者可以通过易于阅读的DSL来快速了解上下文,并通过Eclipse的快捷键F3,快速地在验收条件和技术实现中进行切换,从而更有效率地阅读测试。
* 减少启动功能测试的花费
用户可以通过在Eclipse中创建Twist项目,快速展开测试。 Twist项目包含了运行Twist测试所必需的所有的第三方库。同时,Twist发布了相应的ANT target,大大减少了团队用于启动功能测试的时间。
在使用Twist的过程中,我们也发现了一些问题,主要集中在Twist的IDE不够稳定,会有一些UI的异常等方面。我们已经将BUG提交到了Twist团队的论坛里,相信稍后的版本会更加稳定易用。 通过使用Twist,很好地将团队以功能测试为中心整合在一起,团队中的所有角色可以通过一套IDE来编写、实现、运行、维护测试,大大减少了交流成本,提高了开发的速度,体会了开发之乐。
2008年12月19日星期五
如何正确的使用Mock
首先我不是反Mock者,但确实对使用Mock持比较审慎的态度,因为Mock是非常难于正确使用的, mock最常见的问题在于假设!假设!假设!
有这样一个功能,当有工作的时候,公民需要买需要买医疗保险,住房公积金和养老保险,如果失业了他只需要买养老保险:
相应的Mock测试有两种情况需要覆盖:
* mock people对象, 假设getJob方法返回null,验证insurances中只有包括养老保险
* mock people对象, 假设getJob方法返回Not null,验证insurances中包含四金
在这里的Mock测试进行了假设,它的coorelation people对象在有工作的时候返回非Null的Job对象,而在没有工作的时候返回Null,类似的代码在任何一个项目中都可以找得到踪迹。
问题在于这样的假设可以被悄无声息的破坏掉,假如有人重构了People对象, 在没有工作时返回一个new NullJob()对象(Null Object Pattern), 这样重构后,失业的人也不得不买四金了,然而我们之前编写的mock测试会100%的通过
如何解决这样的问题呢?
功能测试是一个解决思路,因为在Mock测试中,我们不断的在层与层之间做出假设,一定需要一个端到端的测试来验证我们的假设是否正确。功能测试解决了部分的问题, 回顾上面的问题,你发现至少需要编写两个功能测试才能百分之百的发现刚才重构引入的bug, 如果只编写了happen path的功能测试(有People工作的功能测试),那你只有祈祷QA能帮你及时的找到问题了。 不幸的是,由于功能测试的代价比较大,所以大多数的人都只会编写有限的功能测试,往往这些测试仅仅用于覆盖Happy path. 对于如此简单的问题功能测试尚且不能解决问题,更遑论我们“大型企业级超复杂”的项目呢。
另一个方法就是减少假设,回顾一下我们的实现代码:
getJob的返回值是我们需要进行两次假设的根源,如果没有返回会怎样?
在第一个例子中我们需要进行两个假设(Null 和 Not Null),而第二个例子,我们只需要一种假设
第一个例子中稀松平常的代码违反了基本的面向对象的原则“封装”,简而言之是tell do not ask!!原则,因为违反了这个原则,我们不得不做出很多假设,减少假设的一个有效途径就是减少return,tell你的对象替你效劳,如果你对写不出健壮的mock测试烦恼,不妨看看是否写出了符合面向对象原则的产品代码,
正确使用Mock的原则就是尽量不用,改善设计才是王道,代码难于使用通常的方法进行测试而不得不使用mock,绝对是一种smell,不要使用mock来掩盖这种味道。
最后推荐李晓同学的
不要把Mock当作你的设计利器
还有感谢Chris Stevenson今天的Session和帮助。
有这样一个功能,当有工作的时候,公民需要买需要买医疗保险,住房公积金和养老保险,如果失业了他只需要买养老保险:
public void requreInsurance(Insurances insurances) {
if (people.getJob() == null) {
insurances.add(new RetirementInsurance());
} else {
insurances.add(new HealthInsurance());
insurances.add(new RetirementInsurance());
insurances.add(new HouseFund());
insurances.add(new UnemploymentInsurance());
}
}
相应的Mock测试有两种情况需要覆盖:
* mock people对象, 假设getJob方法返回null,验证insurances中只有包括养老保险
* mock people对象, 假设getJob方法返回Not null,验证insurances中包含四金
在这里的Mock测试进行了假设,它的coorelation people对象在有工作的时候返回非Null的Job对象,而在没有工作的时候返回Null,类似的代码在任何一个项目中都可以找得到踪迹。
问题在于这样的假设可以被悄无声息的破坏掉,假如有人重构了People对象, 在没有工作时返回一个new NullJob()对象(Null Object Pattern), 这样重构后,失业的人也不得不买四金了,然而我们之前编写的mock测试会100%的通过
如何解决这样的问题呢?
功能测试是一个解决思路,因为在Mock测试中,我们不断的在层与层之间做出假设,一定需要一个端到端的测试来验证我们的假设是否正确。功能测试解决了部分的问题, 回顾上面的问题,你发现至少需要编写两个功能测试才能百分之百的发现刚才重构引入的bug, 如果只编写了happen path的功能测试(有People工作的功能测试),那你只有祈祷QA能帮你及时的找到问题了。 不幸的是,由于功能测试的代价比较大,所以大多数的人都只会编写有限的功能测试,往往这些测试仅仅用于覆盖Happy path. 对于如此简单的问题功能测试尚且不能解决问题,更遑论我们“大型企业级超复杂”的项目呢。
另一个方法就是减少假设,回顾一下我们的实现代码:
if (people.getJob() == null) {
....
} else {
.....
}
getJob的返回值是我们需要进行两次假设的根源,如果没有返回会怎样?
public void requreInsurance(Insurances insurances) {
people.requreInsurance(insurances);
}
public clas People {
public void requreInsurance(Insurances insurances) {
job.requreInsurance(insurances);
}
}
public class Job {
public void requreInsurance(Insurances insurances) {
insurances.add(new HealthInsurance());
insurances.add(new RetirementInsurance());
insurances.add(new HouseFund());
insurances.add(new UnemploymentInsurance());
}
}
public class NullJob {
public void requreInsurance(Insurances insurances) {
insurances.add(new RetirementInsurance());
}
}
在第一个例子中我们需要进行两个假设(Null 和 Not Null),而第二个例子,我们只需要一种假设
第一个例子中稀松平常的代码违反了基本的面向对象的原则“封装”,简而言之是tell do not ask!!原则,因为违反了这个原则,我们不得不做出很多假设,减少假设的一个有效途径就是减少return,tell你的对象替你效劳,如果你对写不出健壮的mock测试烦恼,不妨看看是否写出了符合面向对象原则的产品代码,
正确使用Mock的原则就是尽量不用,改善设计才是王道,代码难于使用通常的方法进行测试而不得不使用mock,绝对是一种smell,不要使用mock来掩盖这种味道。
最后推荐李晓同学的
不要把Mock当作你的设计利器
还有感谢Chris Stevenson今天的Session和帮助。
2008年12月13日星期六
利用mercurial bisect 二分查找bug
前几天QA找到了一个严重的bug,我们编写了一个测试,并修复了它。 问题是我们在哪个版本引入了这个bug? 在1.0版本的时候,这个功能还是正常工作的,
从1.0版本到现在我们进行了上千次提交,找到引入问题的那个版本,看起来是"不可能"的任务,但是因为mercurial提供了bisect,而变的非常容易。
bisect是一个利用二分法来查找在哪个版本中引入bug的一种方法。大致的思路是先标记一个已知没有bug的版本为"good", 再标记一个已知有bug的版本为"bad", 然后hg会自动将版本更新到这两个版本的中间,然后开发者通过运行测试(自动或者手工)决定标记这个版本是"good"或者"bad", hg会再次决定一个中间版本让我们进行测试,直至找到一个相邻的good版本和bad版本,这个bad版本就是我们引入bug的版本。
举个例子:
假如版本1是没有bug的版本,版本10是有bug的版本,我们是在版本7引入了这样一个bug。hg寻找它的办法是:
然后,hg会自动更新到R1-R10的中间版本R5,通过运行测试,我们知道这是一个没有bug的版本,于是标记这个版本是没有bug的
接着,hg自动更新到版本R5-R10的中间版本R8,通过运行测试,我们知道这是一个有bug的版本,于是标记这个版本为bad
接着,hg自动更新到版本R5-R8的中间版本R6,通过运行测试,我们知道这是一个没有bug的版本,标记这个版本为good
hg自动更新到版本R6-R8的中间版本R7,通过运行测试,我们知道这是一个有bug的版本,标记这个版本
这样,最后hg通过2分法不断缩小范围,我们得到了这样一个结果
这样我们可以得知在R7引入了bug,再分析R7所提交的文件就可以明白当时的错误在哪里了。
这样通过自动化测试和mercurial提供的二分查找,我们可以轻松的找到错误的根源。
从1.0版本到现在我们进行了上千次提交,找到引入问题的那个版本,看起来是"不可能"的任务,但是因为mercurial提供了bisect,而变的非常容易。
bisect是一个利用二分法来查找在哪个版本中引入bug的一种方法。大致的思路是先标记一个已知没有bug的版本为"good", 再标记一个已知有bug的版本为"bad", 然后hg会自动将版本更新到这两个版本的中间,然后开发者通过运行测试(自动或者手工)决定标记这个版本是"good"或者"bad", hg会再次决定一个中间版本让我们进行测试,直至找到一个相邻的good版本和bad版本,这个bad版本就是我们引入bug的版本。
举个例子:
假如版本1是没有bug的版本,版本10是有bug的版本,我们是在版本7引入了这样一个bug。hg寻找它的办法是:
hg bisect init (初始化数据库)
hg bisect bad R10 (标记版本10是有bug的版本)
hg bisect good R1 (标记版本1是没有bug的版本)
然后,hg会自动更新到R1-R10的中间版本R5,通过运行测试,我们知道这是一个没有bug的版本,于是标记这个版本是没有bug的
hg bisect good
接着,hg自动更新到版本R5-R10的中间版本R8,通过运行测试,我们知道这是一个有bug的版本,于是标记这个版本为bad
hg bisect bad
接着,hg自动更新到版本R5-R8的中间版本R6,通过运行测试,我们知道这是一个没有bug的版本,标记这个版本为good
hg bisect good
hg自动更新到版本R6-R8的中间版本R7,通过运行测试,我们知道这是一个有bug的版本,标记这个版本
hg bisect bad
这样,最后hg通过2分法不断缩小范围,我们得到了这样一个结果
R7 bad
R6 good
这样我们可以得知在R7引入了bug,再分析R7所提交的文件就可以明白当时的错误在哪里了。
这样通过自动化测试和mercurial提供的二分查找,我们可以轻松的找到错误的根源。
2008年12月9日星期二
使用Mercurial Queues
昨天在聊持续集成的时候,同事提出一个问题,因为提交很频繁,能不能让持续集成工具忽略掉某些提交,它们只是完成故事若干步骤的一步,让持续集成工具花1个小时来对明知道不需要的版本进行构建太费时间了。
在ThoughtWorks,很多同事都和我一样患有“频繁提交沉迷综合症”,具体症状是每隔30分钟就得运行提交命令, 否则就血压升高,眼眶发干,手心出汗,焦虑...等。 可是偏偏有时候提交不了,要想提交还得修改不知道多少个文件,你有点恐惧了,可还得硬着头皮干。
正如当时在场的同事田乐回答的一样:根本问题不是持续集成工具是否应该支持这样的功能,而是团队选择的版本控制工具(譬如SVN, CVS, P4)对频繁提交没有提供良好的支持。
我们团队使用的是分布式版本管理工具Mercurial,它医好了我的“频繁提交沉迷综合症”,我把药方写在这里以厮患者。
首先编辑.hgrc文件(* mac和linux用户运行vi ~/.hgrc),在[extensions]条目下加入hgext.mq打开mq的扩展。
保存退出,运行
你可以在Console上看到新增了很多queue相关的命令
在mq打开后,如果我们完成了部分修改,可以运行
这样在开发的时候我们可以通过hg qnew命令将baby step不断提交,保持一个干净的工作目录。
但是这些changeset是没有提交到主repository上的。在完成了所有的所有的工作后,
我们可以运行
*hg qremove -r qbase:qtip
*hg push
这样,团队中的其他开发人员就可以得到你的修改了。
在我们运行hg qnew后,hg其实帮我们创建了一个标准的patch文件,并把它保存到了
如果你想回家继续开发,可以拷贝走这些patch文件,在另外一台机器上,使用hg qimport或者patch(linux,mac)都可以重建你的工作目录。
如果你有兴趣,请参看mercurial mq的文档
在ThoughtWorks,很多同事都和我一样患有“频繁提交沉迷综合症”,具体症状是每隔30分钟就得运行提交命令, 否则就血压升高,眼眶发干,手心出汗,焦虑...等。 可是偏偏有时候提交不了,要想提交还得修改不知道多少个文件,你有点恐惧了,可还得硬着头皮干。
正如当时在场的同事田乐回答的一样:根本问题不是持续集成工具是否应该支持这样的功能,而是团队选择的版本控制工具(譬如SVN, CVS, P4)对频繁提交没有提供良好的支持。
我们团队使用的是分布式版本管理工具Mercurial,它医好了我的“频繁提交沉迷综合症”,我把药方写在这里以厮患者。
首先编辑.hgrc文件(* mac和linux用户运行vi ~/.hgrc),在[extensions]条目下加入hgext.mq打开mq的扩展。
[extensions]
hgext.mq =
保存退出,运行
hg --help
你可以在Console上看到新增了很多queue相关的命令
qapplied
qclone
qcommit
qdelete
qdiff
qfold
等
在mq打开后,如果我们完成了部分修改,可以运行
*hg st (查看本地修改,你应该看到所有被修改过的文件)
*hg qnew -m "[message]" -f "[patch name]" (在queue中压入一条changeset)
*hg st (查看本地修改,现在你应该重新得到了干净的本地目录)
这样在开发的时候我们可以通过hg qnew命令将baby step不断提交,保持一个干净的工作目录。
但是这些changeset是没有提交到主repository上的。在完成了所有的所有的工作后,
我们可以运行
*hg qremove -r qbase:qtip
*hg push
这样,团队中的其他开发人员就可以得到你的修改了。
在我们运行hg qnew后,hg其实帮我们创建了一个标准的patch文件,并把它保存到了
工作目录/.hg/patches
如果你想回家继续开发,可以拷贝走这些patch文件,在另外一台机器上,使用hg qimport或者patch(linux,mac)都可以重建你的工作目录。
如果你有兴趣,请参看mercurial mq的文档
订阅:
博文 (Atom)