Scrum敏捷软件开发之技术实践——测试驱动开发TDD
重复无聊的定义
测试驱动开发,英文全称Test-Driven Development,简称TDD,是一种不同于传统软件开发流程的新型的开发方法。它要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码,通过测试来推动整个开发的进行。这有助于编写简洁可用和高质量的代码,并加速开发过程。(来源百度百科)
重复无聊的过程
- 快速新增一个测试(编者注:并非快速)
- 运行所有的测试(有时候只需要运行一个或一部分),发现新增的测试不能通过
- 做一些小小的改动,尽快地让测试程序可运行,为此可以在程序中使用一些不合情理的方法
- 运行所有的测试,并且全部通过
- 重构代码,以消除重复设计,优化设计结构
然而来自维基百科上的过程
解释稍有不同,却十分重要
1.Add a test In test-driven development, each new feature begins with writing a test. To write a test, the developer must clearly understand the feature's specification and requirements. The developer can accomplish this through use cases and user stories to cover the requirements and exception conditions, and can write the test in whatever testing framework is appropriate to the software environment. It could be a modified version of an existing test. This is a differentiating feature of test-driven development versus writing unit tests after the code is written: it makes the developer focus on the requirements before writing the code, a subtle but important difference. 2.Run all tests and see if the new one fails This validates that the test harness is working correctly, that the new test does not mistakenly pass without requiring any new code, and that the required feature does not already exist. This step also tests the test itself, in the negative: it rules out the possibility that the new test always passes, and therefore is worthless. The new test should also fail for the expected reason. This step increases the developer's confidence that the unit test is testing the correct constraint, and passes only in intended cases. 3.Write some code The next step is to write some code that causes the test to pass. The new code written at this stage is not perfect and may, for example, pass the test in an inelegant way. That is acceptable because it will be improved and honed in Step 5. At this point, the only purpose of the written code is to pass the test; no further (and therefore untested) functionality should be predicted nor 'allowed for' at any stage. 4. Run tests If all test cases now pass, the programmer can be confident that the new code meets the test requirements, and does not break or degrade any existing features. If they do not, the new code must be adjusted until they do. 5. Refactor code The growing code base must be cleaned up regularly during test-driven development. New code can be moved from where it was convenient for passing a test to where it more logically belongs. Duplication must be removed. Object, class, module, variable and method names should clearly represent their current purpose and use, as extra functionality is added. As features are added, method bodies can get longer and other objects larger. They benefit from being split and their parts carefully named to improve readability and maintainability, which will be increasingly valuable later in the software lifecycle. Inheritance hierarchies may be rearranged to be more logical and helpful, and perhaps to benefit from recognised design patterns. There are specific and general guidelines for refactoring and for creating clean code.[6][7] By continually re-running the test cases throughout each refactoring phase, the developer can be confident that process is not altering any existing functionality. The concept of removing duplication is an important aspect of any software design. In this case, however, it also applies to the removal of any duplication between the test code and the production code—for example magic numbers or strings repeated in both to make the test pass in Step 3. Repeat Starting with another new test, the cycle is then repeated to push forward the functionality. The size of the steps should always be small, with as few as 1 to 10 edits between each test run. If new code does not rapidly satisfy a new test, or other tests fail unexpectedly, the programmer should undo or revert in preference to excessive debugging. Continuous integration helps by providing revertible checkpoints. When using external libraries it is important not to make increments that are so small as to be effectively merely testing the library itself,[4] unless there is some reason to believe that the library is buggy or is not sufficiently feature-complete to serve all the needs of the software under development.
为什么测试驱动开发(TDD)是无价之宝
它能确保系统中所有代码都可以被测试。如果必须写下所有的代码来应付一个失败的测试,那么即使我们什么也不做,至少也使用TDD达到了100%的代码覆盖率。
反对
“我们在开发一个非常复杂的系统,我需要事先做一些架构工作。”
作为一个微观层面的实践,TDD从来就没有说过,不能有效地和少量前期架构考虑结合起来。
“设计:有意的而又是涌现式的”
问题在于,如果可能,在意识上取得平衡之后,还需要事前进行什么程度的架构上的考虑。
“总是先写测试必定会花更多的时间,我没有那么多时间去浪费。”
数据表明,做TDD比不做TDD多花15%的时间(George 和 Williams 2003)。但是,数据
还表明,TDD会带来更少的缺陷。微软的两个研究表明:因为使用TDD,发现的缺陷数下降了
24%和38%(George 和 Williams 2003)。的确,TDD在开始的时候会花费更多的时间,
但是,通过降低缺陷修改和维护的时间,这些时间都能赚回来。
我-曾经的TDD怀疑论者
“做TDD比不做TDD多花15%的时间”
在初级阶段,我曾学习TDD并同时尝试把TDD应用到实际产品开发的过程中,发现实际多花的时间远远不止15%。
因为影响该指数的因素有很多:
- TDD的过程熟悉
- 学习如何写合适的测试代码
- 学习如何做重构
- 学习如何在特定技术栈下引进合适的测试框架等
笔者认为,这取决于第一次尝试TDD的工程师的成熟度和技术习得能力。对于首次接触此概念的程序员可能需要付出比原来多出100%的时间,
而对处于有成熟TDD体系环境下的程序员,在首次建立TDD的所有测试用例的时间,大概处于10%-40%的范围,具体数值仍然取决于个体成熟度
以及项目本身的特性。然而,我们所增加的时间并不只是建立TDD所有测试用例的时间,维护TDD的所产生的测试用例也是团队所付出的时间成本。
“因为使用TDD,发现的缺陷数下降了24%和38%”
笔者研究过微软的对于TDD研究的两份Paper,本身实验设计有缺陷,不能完全证明缺陷数下降的数值,而只是经验值,环境因素也对此结果有
非常大的影响。但是,随着程序员的成熟度提升,经验上,TDD是能够帮助程序员在交付给QA之前发现更多的问题,或者说能将缺陷扼杀在TDD循
环过程中。
总之
通常情况下,项目本身开发的时间远小于其长期的运营使用时间,期间会随着业务发展不断发生需求变更,随着程序员越来越熟练的使用TDD技
术以及团队敏捷成熟度提高,通过降低缺陷修改和维护的时间,这些时间都能够赚回来。