敏捷质疑: TDD
Q: 为什么通过单元测试发现的 Bug 很少 ?
A: 单元测试不是用来发现 Bug 的, 而是用来预防 Bug 的. 如果采用 TDD, 测试用例完成之时, 产品代码尚未编写, Bug更无从谈起.
Q: 那是否写单元测试就能提高代码质量了 ?
A: 关于这一点, 似乎有人不这么看, <<TDD Opinion: Quality Is a Function of Thought and Reflection, Not Bug Prevention>>. 不错, 代码质量并不必然关联到单元测试, 诸如净室软件开发之类的方法依然可以在没有单元测试的情况下得到高质量的代码, 但这是另外一个问题. 或许主观上, TDD的本质更接近于促使你把质量内建在思维中, 但客观上, 在其它条件都相同的情况下, 单元测试依然能够起到预防 Bug 的作用.
Q: 单元测试怎么能反映/代替需求 ?
A: 单元测试未必能直接反映宏观上的需求, 但
-
功能测试和集成测试能够反映宏观需求.
-
单元测试能够反映系统的其它部分对当前单元的需求.
而从文本的角度, 测试用例的名字就是需求的描述. 换句话说, 你从传统的需求文档中把描述抠出来, 放到测试代码中作为测试用例的名字, 你便拥有了可执行的需求文档
一个 RSpec 写的功能测试用例 (不要怀疑, 它确实是可以运行的):
it "should show welcome message after login" do
login_as_chelsea
get :index
response.should have_text(/欢迎 chelsea/)
end
it "should not show welcome message after logout" do
logout
get :index
response.should_not have_text(/欢迎/)
end
单元测试的例子:
public void testShouldBeFreeFrom2amTo5am() throws Exception { //直接业务需求
...
}
public void testShouldThrowExceptionIfCannotFindConfigFile() throws Exception { //来自系统其它部分的需求
...
}
测试用例并不排斥业务层面的需求文档, 一个高层的, 突出业务价值的需求/愿景描述对于快速理解系统是非常有帮助的, 但/只是测试用例以另一种方式描述了真实的系统, 它具有两个突出的优点:
-
它不会说谎, 即永远与系统真实的行为同步
-
它是可执行的, 它可以不知疲倦的, 成本极低的, 时时刻刻, 反反复复的追问你的系统是否符合需求
Q: 需求变了怎么办? 岂不是有大量测试用例需要修改?
A: 难道不是应该的吗? 难道以前的需求文档在需求发生变化时不需要修改? 哦, 或许它们不需要, 因为没人会关心, 对代码也没什么影响, 需求文档在度过最初的几周后便被扔在配置库里再也没人管它了.
1073x: "这也是很多程序员不愿意作单元测试,并且抵制TDD的原因(虽然他们两者根本不是一回事)。在开发过程中确定什么应该用测试用例描述,什么可以不用,是很重要的。一段产品代码与之对应的测试代码的粒度,数量,都是需要斟酌的问题。"
是的, 这关系到你的测试策略. 然而通常的测试策略对单元测试的要求都是尽可能周全. 于是这就是一个测试设计的问题. 是的,测试代码也需要设计, 也需要重构, 也需要 Domain Specific.
Q: 我的单元测试编译链接速度很慢, 而且有些条件很难测, 比如内存不足, 或者环境很难搭建, 比如需要网络或数据库, 怎么解决?
A: 这是集成测试, 不是单元测试. 你一定把系统所有的组件都编译链接起来了. 那么如果你的测试失败了, 是哪一部分的问题呢?
通常单元测试需要满足一个条件: 不依赖任何其它单元, 即隔离性. 实现手段就是在测试环境中能够轻易的假冒依赖, 并设定依赖按照我们的意愿进行工作. 一个例子就是你的代码依赖 malloc 获取内存, 而你想测试内存不足的情况. 那么我们应在能够/需要在单元测试中使用使用一个假冒的 malloc 来代替真正的 malloc, 并且我们能控制假冒的 malloc 返回 NULL 以模拟内存不足的情况. 关于如何做到这一点, 可参考一些成熟的"假冒"框架, 如 mockcpp 等.
Q: 我原来的测试都是用真实的代码来跑, 一个测试能覆盖多个单元. 你现在都把依赖替换掉了, 那被替换掉的模块有问题怎么办? 怎么保证集成真实的代码后还能正确工作?
A: 其它单元有其它单元自己的单元测试, 各自关注自己. 集成测试像以前一样, 该怎么测还怎么测, 并不是有了单元测试就不要其它测试了.
Q: 单元测试就是设计? 单元测试怎么能反映/代替设计 ?
A: 单元测试反映的是局部的设计, 局限于本单元以及与之交互的其它单元. 前面说的单元测试能够反映系统的其它部分对当前单元的需求, 所谓设计就是单元之间的职责划分, 交互和依赖关系
当你试图测试一个单元时, 却发现需要创建大量的其它对象, 而且按照你脑海中的实现, 有些对象是在单元内部创建的, 根本无法在测试环境中假冒它们. 这时候, 你即使只是为了减少测试的难度, 也会逼迫自己思考:
-
这个单元是否做了太多的事, 承担了额外的职责, 违反了单一职责原则?
-
是否应该把依赖让外界设置进来, 而不是自己在内部创建, 这样测试时就能把依赖设置为假冒的实现?
是的, 单元测试警示你思考一下自己的设计
Q: 单元测试是设计, 还有人说源代码是设计, 到底是测试是设计还是源代码是设计?
A: 这实际上是另外一种角度. 源代码就是设计的论断基于两个假设
-
设计阶段中工程师的工作产物, 也就是他的设计, 是应该能够在实施阶段被不同的实施者严格并且几乎一模一样的实现
-
软件开发人员也是工程师, 即软件工程师
如果我们认同这两个假设, 那么软件工程师的什么产物能够被严格并且重复实现的呢? 是你的Word形式的"设计"文档吗? 是CAD工具画出的UML图吗? 都不是, 因为它们都不精确, 有无数种实现方式, 根本谈不到严格, 不同的开发人员会有完全不同的实现. 事实上, 只有源代码,才能满足这个约束. 这样软件的设计阶段, 就是直到软件工程师完成源代码的那一刻, 而软件的实施阶段, 其实就只剩编译和部署了. 跑题了.
Q: 单元测试是需求"文档", 单元测试又是设计"文档", 它怎么能既是需求又是设计呢?
A: 名字和断言描述需求, 环境设置描述设计 ...
Q: 既然单元测试描述的是需求, 它就应该是黑盒测试了? 可单元测试不一直都被认为是白盒测试吗?
A: 黑白都是相对于你观察的层次. 相对于其它从外部观察"系统"行为, 不涉及源代码的测试来说, 单元测试深入到内部观察盒子的行为, 所以是白盒. 而具体到每个单元测试用例, 依然在尽可能的从外部观察"单元"的行为, 所以又是黑盒.
Q: 但是你们常用的 Mock 技术, 明显把单元测试推向白盒的境地.
A: 说来话长, 但可以先说结论: 基于状态的测试 over 基于交互/行为的测试, 虽然右边的也有巨大的价值, 但我们认为左边的更稳定和更富有对系统的洞察力
基于状态的测试描述的是需求, 基于交互行为的测试描述的是实现. 相对于需求来说, 实现更易发生变化, 尤其在另外一种实践"重构"的冲击下, 描述实现的测试将被修改的面目全非, 带来相当的返工和维护成本
一种例外, 就是交互本身就是需求, 这时 mock 是合适的选择. 一个杜撰的例子参见<<TDD: Tricky Driven Design 3, 方法>>中最后银行API的例子
而现实生活中, 存在一些情况, 虽然使用 mock 可能带来后期的维护成本, 但它带来的好处也是不可代替的. 比如对先期整体测试代码的编码量的降低. 这在 C/C++ 项目中尤其明显:
受限于C/C++的编译模型, 使用常用的预处理期接入点和编译链接接入点技术来接入 stub 实现时, 要小心维护头文件的防卫宏, 头文件的名称, 不同环境下构建脚本的include路径设置, 库路径设置等. 手工写stub的方式变的及其繁琐和容易出错. 这时候, 一个易用的 mock 框架如 mockcpp 等将节省大量的编码和先期维护工作
而几乎所有的mock框架, 都支持将 mock 对象退化为 stub, 如 mockcpp 中 mock 对象的 defaults() 设置, 或者 JMock 2 中的 Allowing . 事实上, 这是我推荐的 mock 使用方式: 通常情况下让它退化为简单的stub, 必要时才使用它强大的期待设置和验证能力.
通常单元测试有两个公认的约束需要满足:
-
快
-
隔离依赖.
重申一遍结论就是: 在满足单元测试的快和隔离依赖的前提下,
-
优先选择基于状态的黑盒测试(可使用手写stub或mock退化的stub)
-
除非交互和行为本身就是需求(可使用mock对象的全部特性)
Q: 怎么测 private 函数?
A: 把它变成 public 的.
我是认真的. 如果发现 private 函数无法简单的通过某个public函数的测试来覆盖而需要专门的测试, 意味着你的单元可能承担了太多的职责, 应该拆分到一个单独的单元中, 并开放为 public 函数.
如果使用 C++, 在测试环境中 #define private public.
如果使用 g++, 在测试环境中加入 -fno-access-control.
Q: 类似 private, 一些意图实现良好设计的语言特性, 如 static, sealed, final, 非虚函数等, 却总是给代码的易测试性带来麻烦, 该如何取舍?
A: 没什么好办法. 这些语言特性和测试的目的是相同的, 都是为提高代码质量, 减少出错的可能, 虽殊途同归, 但却互相限制, 效果也不一样.
我认为工业界是时候严肃认真的考虑测试环境了, 最好在语言中内建对测试的支持, 一些为产品环境设计的语言特性, 应该在测试环境中关闭, 而在产品环境中生效. 其实之前很多编译器都支持 Release 和 Debug 两种环境, 也是从代码质量的方面考虑的. 现在毫无疑问证实单元测试比 Debug 更有效, 是时候与时俱进增加对 Test 的支持而逐渐罢黜对 Debug 的支持.
在语言本身增加对测试的支持之前, 我们不得不想办法在测试环境中绕过语言特性的限制, 尤其对遗留系统, 代码已经存在的情况. 比如对于 C++ 中的 static 函数, 可以将整个被测单元 #include, 或者 #define static 为空. 宏代表了一层间接, 在测试环境中, 这层间接是至关重要的. 其它方法可参考 <<Working Effectively with Legacy Code>>, <<假冒的艺术>>中的介绍.
Q: 刚才提到了要支持"测试"而不是"Debug", 测试和Debug难道有什么矛盾吗?
A: 有. 如果你发现不得不 Debug, 就是测试粒度太粗, 步子迈的太大, 产品代码过长等导致的, 甚至可能你卷入了过多的单元而破坏了测试的隔离性. Debug还是代码逻辑不清, 行为难以断言的表现. 用测试帮你定位错误.
Q: 我知道为遗留系统增加新特性是要先写测试保证系统原来的行为, 可遗留代码很庞大, 我甚至都不知道系统目前的行为, 怎么办?
A: 特征测试: 保持代码行为的测试, 获取当前运行结果, 来填充测试, 以获取系统目前行为. 其实测试可以分为两类: 试图说明想要实现的目标, 或者试图保持代码中既有的行为; 在特性实现后, 前者会转化为后者. 详细信息请参见<<Working Effectively with Legacy Code>>
Q: 有成熟的关于在遗留系统上实践 TDD 或者单元测试的实践吗?
A: 还是<<Working Effectively with Legacy Code>>, 或者<<在大型遗留系统基础上运作重构项目>>
Q: 前面经常说到 C++ 或其它面向对象语言, 却没有提到 C, 那么过程式语言中如何应用 TDD ? 有什么不一样?
A: 基本一样, 并且在过程式语言中应用 TDD, 可能会导出面向对象风格的设计. 比如如果直接调用某个函数, 那么不得不通过编译时替换或链接时替换来接入假的实现. 这样其实比较麻烦, 因此可能会促使你选用函数指针 ,以便方便的在测试环境中进行替换. 随着时间的推移, 你会发现一组组概念相关的函数指针出现了, 那么把它们和它们操作的数据绑定在一起, 定义一个 struct, 就形成了一种对象风格. 当然这反而可能会令你的代码更复杂, 这需要在实践中取舍.
也有可能在过程式语言中你觉得 TDD 对设计的促进不大, 而且测试用例也比较枯燥, 就是测个分支, 返回值什么的. 是的, 逻辑就隐藏在分支和返回值中, 如果习惯了过程式思维并不打算改变, TDD 对设计的影响则更多的体现在依赖管理上, 如头文件和编译单元的职责划分. 如果把不同职责的函数混在一个编译单元里面, 则很难实施链接替换等手段, 除非你选择一个类似 mockcpp 的框架, 不需要链接替换.
Q: 如果使用 TDD, 那么测试人员怎么安排? 是不是一开始就要进入项目组? 可那时还没有产品代码,测什么?
A: 是, 是一开始就要进入项目组, 可不是因为 TDD. 是, 测试人员是一开始没什么可测的, 可不代表就没活干.
TDD是一种开发方法, 是开发人员参与的活动. 其效果是以可执行的形式文档化你的需求, 迫使你分清职责隔离依赖以驱动你的设计, 编织安全网以扼杀Bug在摇篮状态防止逃逸. 可传统测试人员的活动是试图找到已经逃逸的Bug. 这两种活动都是必要的, 而且毫不冲突, 互为补充.
那么测试人员在新的特性还没开发完成之前做什么呢? 除了提前写测试用例, 无论是自动化的还是非自动化的, 而需要测试人员参加的一项重要活动, 就是参与特性验收条件的制定. 之前经常发生开发人员按照自己的理解去编码, 测试人员按照自己的理解去测试, 直到开发完成, 测试过程中才发现理解的不一致, 开始产生争执并阻塞等待业务分析人员(如果幸运的话)或者行政主管(如果开发过程混乱的话)的仲裁. 解决办法就是就在开始开发新特性前的一刹那, 由业务分析人员, 测试人员, 开发人员进行一次讨论, 就验收条件达成一致并形成记录, 然后测试人员和开发人员分头去写测试和实现.
Q: 之前会有一个阶段, 就是一组相关的特性开发完成后, 测试人员接手测试, 几轮Bug修复过去后, 产品基本稳定就可以发布了. 现在测试人员提前介入到每个迭代中, 针对单个特性进行测试, 那如何保证产品集成起来的质量?
A: 跟以前一样, 该有那么个集成测试阶段还得有那么个集成测试阶段, 取决于产品当时的质量状态. 并不是说有了迭代级别, 单个特性级别的测试就不需要发布级别的集成测试了, 两者没有任何矛盾.
Q: 那么测试人员提前进入迭代有什么好处?
A: 尽早发现问题, 降低修复错误的成本. 有几种手段, 一是前面提到与业务人员和开发者一起讨论验收条件, 这样就能防止理解偏差而导致的返工. 二是开发完成立即测试, 发现问题立即反馈, 这样开发人员对代码依然印象深刻,能快速定位和修复错误. 这样流入最后集成测试阶段的Bug就会少, 会缩短最后的集成测试时间, 保证产品更平稳的发布.
Q: 有时候后续的特性会影响前面的特性, 那么迭代过程中测试人员只测单个特性, 怎么保证以前的特性依然工作?
A: 几个手段. 测试尽量自动化, 以便能够持续集成. 再就是做好依赖管理, 每当一个新特性完成, 就应该能够发现它影响的其它特性, 看看是否应该补充一些集成测试.
Q: 有时候开发人员完成一个特性时已接近迭代结束, 测试人员没有时间进行充分测试, 怎么办?
A: 下个迭代测呗, 并且在计算开发速度时, 只应该计算本迭代通过测试人员验收的特性, 那些仅仅是开发人员完成, 没有经过测试人员充分测试的特性不计在内. 这种情况是不可避免的. 但我们能通过一些手段让测试与开发更加同步, 尽量缩短滞后性, 包括让测试人员与开发人员更紧密合作, 尽量让测试用例自动化等.
Q: 我还是觉得在开发迭代过程中, 测试人员的工作量不饱满.
A: 如果这不是您的感觉, 而是事实, 并且前面测试人员必须要做的工作也都做了, 还是不饱满, 那么恭喜你, 可以省下一些测试人员, 去做别的事了. 但不推荐的是, 不要让测试人员同时为两个团队工作. 这会大大增加沟通的成本. 你会经常发现, 当你的开发者想找测试人员协助时, 却找不到人了, 于是你的团队便被堵塞在那里. 而测试人员本身的Context切换也是痛苦的.
Q: 你们说验收测试应该由客户来编写, 可在我们这里根本不可能.
A: 验收, 当然是由客户来验收, 这在理论上是毫无疑问的, 而且肯定在各行各业发生着. 只是具体到测试用例的编写和执行, 无论是自动化的还是非自动化的, 都需要掌握一定的技术, 需要周密的思考, 需要专门的时间, 客户可能无法同时满足这几个条件, 我们要尽力争取, 争取不到, 便只好通过更充分的交流来弥补越俎代庖的失真. 这时业务分析人员和测试人员要通力合作, 完成验收测试的编写.
Q: 你们说你们之前的项目产品代码和测试代码的比例大约 1:3, 这不是平白增加了 3 倍的工作量吗?
A: 是增加了 3 倍的代码量而不是工作量. 它节省了你几十人做几个月庞大的预先设计的工作量, 节省了你详细设计每个模块并为之编写几百页详设文档的时间, 节省了无数不眠之夜通宵Debug的时间, 它节省了集成阶段修复难以计数的Bug的工作量, 甚至它缩减了你产品代码的数量, 大量的重复代码被消除了, 大量过度设计的复杂代码被废除了, 你的代码更易理解了, 添加新特性更容易了, 发现的Bug更易定位了, 以致于大大减少了长达数年的生命周期内维护的工作量. 有点夸张了? 可这就是 TDD 和敏捷开发带给我们的好处(如果你已经实践了)和vision(如果你还在观望)
Q: 我们也做单元测试, 但是是先写产品代码后写测试的. 难道非得 TDD, 非得测试先行吗?
A: 没什么事是非做不可的. 取决于你要什么. TDD 只是以可验证的方式迫使你将质量内建在思维中, 长期的测试先行将历练你思维的质量. 而事后的单元测试只是惶恐的跟随者.
文章原始出处 http://blog.csdn.net/chelsea/archive/2008/07/13/2646431.aspx
相关推荐
##### 测试驱动开发 (TDD) - **单元测试框架**:常用的框架有 CppUnit、CppUnitLite、Boost.Test 和 CxxTest。这些框架帮助开发者编写和组织单元测试,确保代码的质量。 - **Mock 框架**:例如 mockpp、revised-...
此外,书中可能还会探讨持续集成、测试自动化策略以及如何将TDD融入敏捷开发流程。 总之,通过这本书的学习,Java开发者不仅能提升自身的测试技能,还能进一步优化开发流程,为项目的成功提供坚实的基础。
然而,DHH(David Heinemeier Hansson)在其文章《TDD已死,长久测试》中表达了对TDD的质疑,认为TDD已经不再适用于现代软件开发的某些场景。以下是对TDD的详细解读以及它在解决编程问题中的作用: 1. **防止过度...
TDD-CDMA 技术及在 4G 中的应用 TDD-CDMA 技术是第四代无线通信系统(4G)中的一种关键技术。TDD-CDMA 是一种 Time Division Duplexing Code Division Multiple Access(时分双工码分多址)技术,具有高频谱效率、...
3. 在Java中使用TDD: - **JUnit**:JUnit是Java最常用的单元测试框架,用于编写和运行测试用例。了解断言、测试注解(如@Test、@Before、@After)以及如何组织测试类是关键。 - **Mockito**:Mockito允许我们创建...
《Spring Boot TDD入门实践详解》 在编程领域,Test-Driven Development(TDD,测试驱动开发)是一种软件开发方法,它强调先编写测试用例,再编写满足这些测试用例的代码。在这个名为"spring-boot-tdd-example"的...
在《Test-Driven and Acceptance TDD for Java Developers》这本书中,作者Lasse Koskela可能介绍了如何将模型驱动的方法与测试驱动开发(TDD)相结合,以便于Java开发者能够更有效地构建高质量的应用程序。...
**TDD_dice: 探索TDD在Ruby中的实践** TDD(Test-Driven Development,测试驱动开发)是一种软件开发方法论,它强调先编写测试用例,再编写实现功能的代码,以此确保代码的质量和可维护性。在这个名为"TDD_dice"的...
标题 "TDD-first" 暗示我们正在讨论的是测试驱动开发(Test-Driven Development, 简称TDD)的初步介绍。TDD是一种软件开发方法论,它强调在编写实际代码之前先编写测试。这种方法有助于确保代码的质量,因为它要求...
极限编程是另一个重要的敏捷框架,包括测试驱动开发(TDD)、结对编程、持续集成等实践,这些方法旨在提高代码质量,减少缺陷,并促进团队协作。 5. **设计模式**: 在敏捷开发中,设计模式是解决常见问题的有效...
1. **敏捷开发**:敏捷开发是一种以人为本,迭代且适应变化的软件开发方法论。它强调快速响应需求变化,提高团队协作效率,重视软件的可维护性和质量。敏捷宣言是其核心,包括四个价值观:个体和互动高于流程和工具...
- **案例**: 例如IBM等大型企业在敏捷转型过程中将TDD视为提升开发效率和软件质量的关键手段之一。 综上所述,TDD作为一种现代软件开发方法论,其核心理念和实践流程对于提高软件开发的效率和质量有着显著的作用。...
【标题】"IncubyteAssessment:TDD评估资料库"是一个针对软件开发过程中的测试驱动开发(TDD)技术的评估资源集合。TDD是一种编程实践,它强调先编写测试,然后编写最小化的生产代码来使测试通过。这个资料库可能是为了...
标题 "KandibandaSVLNRSAketh_TDD-Junit-:TDD和Junit任务" 提示我们,这是一个关于测试驱动开发(Test-Driven Development, TDD)和JUnit框架的学习项目。在这个项目中,我们将深入理解TDD的概念、原则以及如何利用...
- **测试驱动开发(TDD)**:开发者先编写测试用例,然后编写代码以通过这些测试,确保代码质量。 - **结对编程**:两名程序员共享一个工作站,一人编码,另一人审查,促进知识共享和即时问题解决。 - **持续集成**...
在IT行业中,Test-Driven Development(TDD)是一种软件开发方法论,它的核心思想是先编写测试用例,再编写实现代码,确保代码能够通过已有的测试。TDD强调"测试先行",有助于提高代码质量,减少缺陷,并促进设计的...
在软件工程领域,Test-Driven Development(TDD)是一种编程实践,它强调先编写测试用例,再根据这些测试来实现功能代码。这种方式确保了代码的质量和可维护性。在这个"String-Calculator:TDD练习-软件工程"项目中,...
**TDD(Test-Driven Development,测试驱动开发)**是一种软件开发方法,它强调在编写实际的代码之前先编写测试。TDD 的核心理念是通过编写单元测试来定义功能需求,然后根据这些测试来实现代码,确保代码的质量和...
标题中的“String-Calculator:TDD分配”指的是一个编程项目,其中的任务是实现一个字符串计算器,并遵循测试驱动开发(Test-Driven Development, TDD)的方法。TDD是一种软件开发过程,它要求开发者首先编写测试,...