关于测试要多细、TDD、敏捷开发等的思考

725 阅读7分钟

TDD

TDD的核心理念是什么呢?第一是Specification by Example,即把测试用例作为表达需求的一种方式,作为传统的文档,Use Case等的一种补充。“不把测试用例单纯地视为测试,而从需求和设计的角度来看测试用例”的理念本身是好的。

TDD的第二个理念是Test First,强调测试对于实现的驱动作用,先写测试用例,再实现和重构。在Specification by Example的理念下,Test First的实质是“先理解清楚需求,并做好外部接口设计,把它转化为测试用例,然后再来实现和重构”。

如果片面强调测试对实现的驱动作用,那么实际上隐含了“需求可以在实现之前固定下来”的假设,这是非常不敏捷的和不现实的!我认为要做到真正的敏捷必须承认 “需求无法在用户真正能运行看到效果之前明确下来“由此可见,Test First和瀑布式思想没有区别,都强调需求先于实现,而忽略了软件需求的产生是一个在实际运行中不断调整探索完善的过程。除了简单情况,你能够在明确了需求之后就实现出一套linux系统吗?既然你根本无法实现一套linux系统,那么这样所谓的需求又有多大的意义呢?所以,能提出什么样的需求不能脱离你的实现能力。需求和实现之间不是简单的谁驱动谁,而是一种相互反馈的关系,这与需求用什么方式表达没有关系。到目前为主,我推崇的方式是快速实现,在实际运行中体验效果,不断优化探索和明确需求,当需求达到一个比较稳定的程度才编写测试用例将需求固化下来。

when多一点测试?

  • 极其容易出错的部分,应该编写足够的测试。

    • 复杂的条件逻辑,比如反复嵌套的条件判断
    • 你日常容易犯错的地方
    • 你不确定的地方
  • 与具体业务关系不大的需求,比如:写一个通用的数据结构,实现一个通用算法。TDD的先关注需求和思考外部接口设计的理念也对促进开发人员的抽象思维有很大益处。

  • API和类库,更方便用单元测试去保证正确性。

  • 对于已经出现的bug,应该通过测试去复现,并用测试去保障修复完成。从常理上讲,如果一个bug出现了一次,那么极有可能再多次反复出现。

when少一点测试?

  • 需求如果变化非常快,那么不应该花大量的时间去写测试。因为测试是基于程序员对功能的假设而编写的,在这个假设不一定完全正确和完善的情况下,我们的测试是意义不大
  • 框架设计大于测试。当我们的软件框架还没确定下来的时候,就基于去编写测试来保证正确性是愚蠢的。仅仅通过测试来保障程序的正确性,而越少框架设计,会让代码变得不可维护。因此少一点时间写测试,多一点时间做设计。
  • 永远要承认,经过测试的程序并不是没有bug的。我们需要从框架的角度去减少bug。因此少一点时间写测试,多一点时间做设计。

观点碰撞

敏捷开发 VS 测试先行

先讲一讲两者:

  • 敏捷开发总是假设需求是频繁变化的,且是需求多轮迭代才可确定下来的。因此要求我们尽快写出可以运行的程序,交付给客户来看。
  • 测试先行要求我们先假设好需求外部接口,然后先写测试,再写代码。这样之后重构和优化都可以更加放心。

这里的矛盾在于,敏捷开发认为要尽快可运行的程序,然后来迭代需求,而测试先行则需要基于正确、完善的需求,先写大量的测试。编写大量的测试势必会花费大量的时间,延迟交付;而不断变化的需求也会让测试代码不断发生改变,进一步延迟每一期的交付。除此之外,两者还有这些矛盾点:

  • 两者对于需求的假设是矛盾的;敏捷开发假设需求一直变,测试先行假设需求不变,我的实现变。
  • 两者的目的是矛盾的;敏捷开发的目的是尽快完善和迭代需求。测试先行的目的是简化维护、简化优化和重构的心智负担。

单元测试 VS 集成测试

在这个讨论中,我们假设需求是确定的,稳定的。

这里的矛盾点在于,编写单元测试会推迟集成测试的时间。如果我们想尽快进行集成测试,且我们假设有些事情必须在集成测试之后才会被暴露,那么我们应该少些一点单元测试,尽快先进行集成测试

但是上述的这个假设并不总是成立。在一个简单的系统中,没什么必须要在集成测试之后才能确定的事情。通过合理的设计,多端(比如前后端、上下游端)的沟通,可以在编码前就确定好外部接口。

high level VS low level

这里是指在一个代码项目中,高层设计的测试与底层设计的测试。

  • 如果写的太过High Level,那么,当你的Test Case失败的时候,你不知道哪里出问题了,你得要花很多精力去debug代码。而我们希望的是其能够告诉我是哪个模块出的问题。只有High Level的Test Case,岂不就是审视所有环节?
  • 如果写的太过Low Level,那么,带来的问题是,你需要花两倍的时间来维护你的代码,一份给test case,一份给实现的功能代码。
  • 另外,如果写得太Low Level,根据Agile的迭代开发来说,你的需求是易变的,很多时候,我们的需求都是开发人员自己做的Assumption。所以,你把Test Case 写得越细,将来,一旦需求或Assumption发生变化,你的维护成本也是成级数增加的。

自顶向下 VS 从底向上

先说任何一个大项目,总是要自顶向下地拆分任务,这几乎是不可避免的。但是在实现的时候,自顶向下却可能会出现问题。

这种问题在一些人们不那么熟悉的领域中尤其常见。因为在不熟悉的领域中,任何一个细小的细节问题都可能成为单点故障,导致整个项目的崩溃和失败。这里以挑战者号的失败为例

自顶向下设计,但是从底向上实现。先有个可动雏型,再不断重构翻修整体构架。换句话说,先来个v1自顶向下的设计,从底层实现到上层。再来v2自顶向下的设计,…,如此循序渐进改进整个系统。

回到测试上,我们会发现高层的测试无法保证不出现单点故障而让整个系统崩溃,反而可能忽略掉一些底层问题。底层的测试可以保证单个模块不出问题,但是也保障不了模块间的互动不出问题。因此测试和自顶向下还是从底向上都没什么关系,TDD测试驱动开发也驱动不了解决难题。这里的关键问题在于:对于复杂,没有预设经验的项目而言,只有做好总体设计,然后从底层一点一点向上构建,用单元测试来保障每一层模块的正确性,才能构建成功这个高难度的系统。没有捷径可言。

参考资料

“单元测试要做多细”

虚拟座谈会:TDD 有多美?

[转]TDD到底美还是不美?

RICHARD FEYNMAN, 挑战者号, 软件工程