敏捷质疑:TDD

发表于:2008-7-15 11:52

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:切尔斯基    来源:blogjava

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: 单元测试未必能直接反映宏观上的需求, 但

        1、功能测试和集成测试能够反映宏观需求.

        2、单元测试能够反映系统的其它部分对当前单元的需求.

        而从文本的角度, 测试用例的名字就是需求的描述. 换句话说, 你从传统的需求文档中把描述抠出来, 放到测试代码中作为测试用例的名字, 你便拥有了可执行的需求文档

        一个 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 { //来自系统其它部分的需求

  ...

}

        测试用例并不排斥业务层面的需求文档, 一个高层的, 突出业务价值的需求/愿景描述对于快速理解系统是非常有帮助的, 但/只是测试用例以另一种方式描述了真实的系统, 它具有两个突出的优点:

        1、它不会说谎, 即永远与系统真实的行为同步

        2、它是可执行的, 它可以不知疲倦的, 成本极低的, 时时刻刻, 反反复复的追问你的系统是否符合需求

Q: 需求变了怎么办? 岂不是有大量测试用例需要修改?

A: 难道不是应该的吗? 难道以前的需求文档在需求发生变化时不需要修改?  哦, 或许它们不需要, 因为没人会关心, 对代码也没什么影响, 需求文档在度过最初的几周后便被扔在配置库里再也没人管它了.

Q: 我的单元测试编译链接速度很慢, 而且有些条件很难测, 比如内存不足, 或者环境很难搭建, 比如需要网络或数据库, 怎么解决?

A: 这是集成测试, 不是单元测试. 你一定把系统所有的组件都编译链接起来了. 那么如果你的测试失败了, 是哪一部分的问题呢?

        通常单元测试需要满足一个条件: 不依赖任何其它单元, 即隔离性. 实现手段就是在测试环境中能够轻易的假冒依赖, 并设定依赖按照我们的意愿进行工作. 一个例子就是你的代码依赖 malloc 获取内存, 而你想测试内存不足的情况. 那么我们应在能够/需要在单元测试中使用使用一个假冒的 malloc 来代替真正的 malloc, 并且我们能控制假冒的 malloc 返回 NULL 以模拟内存不足的情况. 关于如何做到这一点, 可参考一些成熟的"假冒"框架, 如 mockcpp 等.

Q: 我原来的测试都是用真实的代码来跑, 一个测试能覆盖多个单元. 你现在都把依赖替换掉了, 那被替换掉的模块有问题怎么办? 怎么保证集成真实的代码后还能正确工作?

A: 其它单元有其它单元自己的单元测试, 各自关注自己. 集成测试像以前一样, 该怎么测还怎么测, 并不是有了单元测试就不要其它测试了.

Q: 单元测试就是设计? 单元测试怎么能反映/代替设计 ?

A: 单元测试反映的是局部的设计, 局限于本单元以及与之交互的其它单元. 前面说的单元测试能够反映系统的其它部分对当前单元的需求, 所谓设计就是单元之间的职责划分, 交互和依赖关系

        当你试图测试一个单元时, 却发现需要创建大量的其它对象, 而且按照你脑海中的实现, 有些对象是在单元内部创建的, 根本无法在测试环境中假冒它们. 这时候, 你即使只是为了减少测试的难度, 也会逼迫自己思考:

        1、这个单元是否做了太多的事, 承担了额外的职责, 违反了单一职责原则?

        2、是否应该把依赖让外界设置进来, 而不是自己在内部创建, 这样测试时就能把依赖设置为假冒的实现?

        是的, 单元测试警示你思考一下自己的设计

Q: 单元测试是设计, 还有人说源代码是设计, 到底是测试是设计还是源代码是设计?

A: 这实际上是另外一种角度. 源代码就是设计的论断基于两个假设

        1、设计阶段中工程师的工作产物, 也就是他的设计, 是应该能够在实施阶段被不同的实施者严格并且几乎一模一样的实现

        2、软件开发人员也是工程师, 即软件工程师

        如果我们认同这两个假设, 那么软件工程师的什么产物能够被严格并且重复实现的呢? 是你的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, 必要时才使用它强大的期待设置和验证能力.

21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号