所以,在敏捷环境中,软件开发初期应该通过文档和用例等手段大致表达需求,实现之后在实际运行中体验效果,不断优化探索和明确需求和外部环境,当需求和对外部环境的认识达到一个比较稳定的程度才编写测试用例将需求固化下来。
上面的论述主要针对贴近最终用户的外部需求(如ATDD),下面我会进一步解释即使是在内部的单元测试级别TDD仍然有问题。我们还是首先从需求入手,思考一下单元的需求是哪里来的呢?答案是:需求来自于设计!比如,对轮胎的需求来源于汽车的设计,低层模块的需求来源于高层模块的设计。
而在开发初期,这种内部设计具有很大的不稳定性,带有很多假设的成分,在没有进行集成测试的情况下,很难讲这种内部设计是否合理。实际项目开发通常会在集成运行之后不断调整内部的设计,即影响单元的需求。那么,如果是测试驱动,首先按不成熟的内部设计把一个个单元需求编写成单元测试再来实现,实际上大大推迟了能进行集成测试的时间,对于真正快速弄清高层需求稳定设计反而是不利的。
假设最终还是所有单元都完成,然后开始运行集成或验收测试,这时候有两种可能:
1. 用户看到实际效果,决定调整需求;
2. 发现集成前在单元层面的假设不成立或者是有没有考虑到的情况。不论是哪一种情况发生,以前所写的单元测试都面临着被废弃或必须修改的命运。实际上,多数与业务相关的单元测试用例比起集成或验收测试用例更加不稳定,因为它会受到所有其上层模块的需求和设计变动的影响。
由于我们在不稳定的单元测试上浪费了大量的时间(按我的经验编写单元测试比编写实现更耗时),这就导致了迟迟无法进行集成看到实际效果,也没有办法敏捷地应对需求的调整。也就是说具有讽刺意味的,Test First理念居然是和敏捷理念矛盾的!
所以,我认为Test First不符合敏捷开发的基本假设,而真正符合敏捷的理念是需求和设计依赖于实现的反馈,需要在实际运行过程中根据效果不断探索调整得来的,不可能脱离实际运行写出真正符合最终需求的测试用例来。所以,我们真正应该做的是尽快看到实际运行的效果,而自动化测试作为固化的需求和设计是在看到效果之后。在集成之前花太多精力进行测试驱动只会导致迟迟看不到实际运行效果(尤其是基于开发人员自己的假设编写大量单元测试用例),看到效果需要调整需求又会废掉或改掉一大堆的测试用例。
实际上,越是外部的需求其变更带来的影响和代价越大,越是需要尽早明确。从宏观上看,TDD所谓的快速反馈实际上是加快内部反馈,延迟了外部反馈,这无异于本末倒置。而大量需要修改或作废的测试用例其实是一种很大的浪费,这和消除浪费的精益思想也是矛盾的!
上面这幅cost/length_of_feedback_cycle图是我们常见的用于说明敏捷方法比传统方法具有更短的反馈周期,更小代价的应对变化。从图中我们可以清晰的看到在验收测试中发现的需求错误导致的代价是最高的。如果验收测试往后推迟一点,发现错误的代价将按非线性地增长。上面我们已经论述了,任何方法都不可能消除验收测试后对需求的调整,因为这是需求产生的正常过程。
我们唯一可以做的是尽可能地缩短验收测试的反馈周期,但是很不幸TDD大量的内部测试只会导致推迟验收测试的时间,从而大大增加代价。在实际开发中,我提倡在第一次集成运行测试之前不要写单元测试用例;自动化的验收测试用例则视编写和维护的代价而定,如果代价比较高,则应该采用文档和Use Case来描述需求,因为这两种方式比自动化的验收测试更容易维护。编写单元测试一定是在集成以后,这样才能首先得到外部反馈,尽量先保证做正确的事情,再正确地做事。
下面这段话来自于InfoQ文章《Mock不是测试的银弹》:在使用JMock框架后测试编写起来更容易,运行速度更快,也更稳定,然而出乎意料的是产品质量并没有如我们所预期的随着不断添加 的测试而变得愈加健壮,虽然产品代码的单元测试覆盖率超过了80%,然而在发布前进行全面测试时,常常发现严重的功能缺陷而不得不一轮轮的修复缺陷、回归 测试。为什么编写了大量的测试还会频繁出现这些问题呢? 这描述的情况和我在实践中遇到的情况类似,不过很可惜文章并没有找到问题真正的原因。
真正的原因不是什么Mock不Mock,而是TDD的单元测试是基于开发人员的假设,这些假设的测试即使全部通过代码覆盖率100%,到了集成测试发现假设根本不成立或者原先在单元层面很多情况没有考虑到,这又怎能保证高质量?在TDD的实践者中我见到过不少类似这样的,他们很认真,编写了很多单元测试用例,代码覆盖率也很高,但他们其实是有意无意在先正确地做事(单元测试),再做正确的事(集成测试),这就是本末倒置。
当然,我不是全盘否定TDD。TDD在某些需求比较固定的场合是适用的,尤其是与具体业务关系不大的需求,比如:写一个通用的数据结构,实现一个通用算法。TDD的先关注需求和思考外部接口设计的理念也对促进开发人员的抽象思维有很大益处。
另外,TDD通常也具有较高的代码覆盖率。本文的主要观点在于:实际项目中,由于用户需求不确定性和外部环境不确定性,不要期望可以在实现之前完全明确需求,需求是在实际运行看到效果之后才逐步明确的;我们的开发过程必须能够敏捷地适应需求的变化,而TDD的Test First理念恰好与之矛盾。
所以,对于TDD不了解的朋友,我建议应该学习和实践TDD,从而获得其益处;同时我也提醒TDD存在理论上的缺陷,这是在实践中需要特别留意的。