Q: 为什么通过单元测试发现的 Bug 很少 ?
A: 单元测试不是用来发现 Bug 的, 而是用来预防 Bug 的. 如果采用 TDD, 测试用例完成之时, 产品代码尚未编写, Bug更无从谈起.
Q: 刚才提到了要支持"测试"而不是"Debug", 测试和Debug难道有什么矛盾吗?
A: 有. 如果你发现不得不 Debug, 就是测试粒度太粗, 步子迈的太大, 产品代码过长等导致的, 甚至可能你卷入了过多的单元而破坏了测试的隔离性. Debug还是代码逻辑不清, 行为难以断言的表现. 用测试帮你定位错误.
Q: 那是否写单元测试就能提高代码质量了 ?
A: 不错, 代码质量并不必然关联到单元测试, 诸如净室软件开发之类的方法依然可以在没有单元测试的情况下得到高质量的代码, 但这是另外一个问题. 或许主观上, TDD的本质更接近于促使你把质量内建在思维中, 但客观上, 在其它条件都相同的情况下, 单元测试依然能够起到预防 Bug 的作用.
Q: 单元测试怎么能反映/代替需求 ?
A: 单元测试未必能直接反映宏观上的需求, 但
–功能测试和集成测试能够反映宏观需求.
–单元测试能够反映系统的其它部分对当前单元的需求.
而从文本的角度, 测试用例的名字就是需求的描述. 换句话说, 你从传统的需求文档中把描述抠出来, 放到测试代码中作为测试用例的名字, 你便拥有了可执行的需求文档
测试用例并不排斥业务层面的需求文档, 一个高层的, 突出业务价值的需求/愿景描述对于快速理解系统是非常有帮助的, 但是测试用例以另一种方式描述了真实的系统, 它具有两个突出的优点:
1、它不会说谎, 即永远与系统真实的行为同步
2、它是可执行的, 它可以不知疲倦的, 成本极低的, 时时刻刻, 反反复复的追问你的系统是否符合需求
Q: 需求变了怎么办? 岂不是有大量测试用例需要修改?
A: 难道不是应该的吗? 难道以前的需求文档在需求发生变化时不需要修改? 哦, 或许它们不需要, 因为没人会关心, 对代码也没什么影响, 需求文档在度过最初的几周后便被扔在配置库里再也没人管它了.
“这也是很多程序员不愿意作单元测试,并且抵制TDD的原因(虽然他们两者根本不是一回事)。在开发过程中确定什么应该用测试用例描述,什么可以不用,是很重要的。一段产品代码与之对应的测试代码的粒度,数量,都是需要斟酌的问题。”
是的, 这关系到你的测试策略. 然而通常的测试策略对单元测试的要求都是尽可能周全. 于是这就是一个测试设计的问题. 是的,测试代码也需要设计, 也需要重构, 也需要 Domain Specific.
Q: 我的单元测试编译链接速度很慢, 而且有些条件很难测, 比如内存不足, 或者环境很难搭建, 比如需要网络或数据库, 怎么解决?
A: 这是集成测试, 不是单元测试. 你一定把系统所有的组件都编译链接起来了. 那么如果你的测试失败了, 是哪一部分的问题呢?
通常单元测试需要满足一个条件: 不依赖任何其它单元, 即隔离性. 实现手段就是在测试环境中能够轻易的假冒依赖, 并设定依赖按照我们的意愿进行工作. 关于如何做到这一点, 可参考一个些成熟的"假冒"框架, 如 mock等。
Q: 但是你们常用的 Mock 技术, 明显把单元测试推向白盒的境地.
A: 说来话长, 但可以先说结论: 基于状态的测试 vs 基于交互/行为的测试, 虽然右边的也有巨大的价值, 但我们认为左边的更稳定和更富有对系统的洞察力
基于状态的测试描述的是需求, 基于交互行为的测试描述的是实现. 相对于需求来说, 实现更易发生变化, 尤其在另外一种实践"重构"的冲击下, 描述实现的测试将被修改的面目全非, 带来相当的返工和维护成本
一种例外, 就是交互本身就是需求, 这时 mock 是合适的选择.
而现实生活中, 存在一些情况, 虽然使用 mock 可能带来后期的维护成本, 但它带来的好处也是不可代替的.
通常单元测试有两个公认的约束需要满足:
–快
–隔离依赖.
重申一遍结论就是: 在满足单元测试的快和隔离依赖的前提下,
优先选择基于状态的黑盒测试(可使用手写stub或mock退化的stub)
除非交互和行为本身就是需求(可使用mock对象的全部特性)
Q: 我原来的测试都是用真实的代码来跑, 一个测试能覆盖多个单元. 你现在都把依赖替换掉了, 那被替换掉的模块有问题怎么办? 怎么保证集成真实的代码后还能正确工作?
A: 其它单元有其它单元自己的单元测试, 各自关注自己. 集成测试像以前一样, 该怎么测还怎么测, 并不是有了单元测试就不要其它测试了.
Q: 单元测试就是设计? 单元测试怎么能反映/代替设计 ?
A: 单元测试反映的是局部的设计, 局限于本单元以及与之交互的其它单元. 前面说的单元测试能够反映系统的其它部分对当前单元的需求, 所谓设计就是单元之间的职责划分, 交互和依赖关系
当你试图测试一个单元时, 却发现需要创建大量的其它对象, 而且按照你脑海中的实现, 有些对象是在单元内部创建的, 根本无法在测试环境中假冒它们. 这时候, 你即使只是为了减少测试的难度, 也会逼迫自己思考:
这个单元是否做了太多的事, 承担了额外的职责, 违反了单一职责原则?
是否应该把依赖让外界设置进来, 而不是自己在内部创建, 这样测试时就能把依赖设置为假冒的实现?
是的, 单元测试警示你思考一下自己的设计
Q: 单元测试是设计, 还有人说源代码是设计, 到底是测试是设计还是源代码是设计?
A: 这实际上是另外一种角度. 源代码就是设计的论断基于两个假设
1、设计阶段中工程师的工作产物, 也就是他的设计, 是应该能够在实施阶段被不同的实施者严格并且几乎一模一样的实现
2、软件开发人员也是工程师, 即软件工程师
如果我们认同这两个假设, 那么软件工程师的什么产物能够被严格并且重复实现的呢? 是你的Word形式的"设计"文档吗? 是CAD工具画出的UML图吗? 都不是, 因为它们都不精确, 有无数种实现方式, 根本谈不到严格, 不同的开发人员会有完全不同的实现. 事实上, 只有源代码,才能满足这个约束. 这样软件的设计阶段, 就是直到软件工程师完成源代码的那一刻, 而软件的实施阶段, 其实就只剩编译和部署了. 跑题了.
Q: 单元测试是需求"文档", 单元测试又是设计"文档", 它怎么能既是需求又是设计呢?
A: 名字和断言描述需求, 环境设置描述设计 …
Q: 既然单元测试描述的是需求, 它就应该是黑盒测试了? 可单元测试不一直都被认为是白盒测试吗?
A: 黑白都是相对于你观察的层次. 相对于其它从外部观察"系统"行为, 不涉及源代码的测试来说, 单元测试深入到内部观察盒子的行为, 所以是白盒. 而具体到每个单元测试用例, 依然在尽可能的从外部观察"单元"的行为, 所以又是黑盒.
Q: 怎么测 private 函数?
A: 把它变成 public 的.
我是认真的. 如果发现 private 函数无法简单的通过某个public函数的测试来覆盖而需要专门的测试, 意味着你的单元可能承担了太多的职责, 应该拆分到一个单独的单元中, 并开放为 public 函数.
Q: 类似 private, 一些意图实现良好设计的语言特性, 如 static, sealed, final, 非虚函数等, 却总是给代码的易测试性带来麻烦, 该如何取舍?
A: 没什么好办法. 这些语言特性和测试的目的是相同的, 都是为提高代码质量, 减少出错的可能, 虽殊途同归, 但却互相限制, 效果也不一样.
我认为工业界是时候严肃认真的考虑测试环境了, 最好在语言中内建对测试的支持, 一些为产品环境设计的语言特性, 应该在测试环境中关闭, 而在产品环境中生效. 其实之前很多编译器都支持 Release 和 Debug 两种环境, 也是从代码质量的方面考虑的. 现在毫无疑问证实单元测试比 Debug 更有效, 是时候与时俱进增加对 Test 的支持而逐渐罢黜对 Debug 的支持.
在语言本身增加对测试的支持之前, 我们不得不想办法在测试环境中绕过语言特性的限制, 尤其对遗留系统, 代码已经存在的情况.
Q: 我知道为遗留系统增加新特性是要先写测试保证系统原来的行为, 可遗留代码很庞大, 我甚至都不知道系统目前的行为, 怎么办?
A: 特征测试: 保持代码行为的测试, 获取当前运行结果, 来填充测试, 以获取系统目前行为. 其实测试可以分为两类: 试图说明想要实现的目标, 或者试图保持代码中既有的行为; 在特性实现后, 前者会转化为后者.
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 只是以可验证的方式迫使你将质量内建在思维中, 长期的测试先行将历练你思维的质量. 而事后的单元测试只是惶恐的跟随者.
文章探讨了单元测试的主要目的不是为了发现Bug,而是预防Bug,强调TDD如何促进高质量代码的编写。单元测试反映了系统需求和设计,但应与集成测试和功能测试相结合。面对需求变化,测试用例需要相应调整。文章还讨论了Mock在测试中的作用,以及如何处理复杂的测试场景。此外,指出测试人员应早期介入项目,以提高效率和产品质量。
2952

被折叠的 条评论
为什么被折叠?



