您还未登录! 登录 | 注册 | 帮助  

您的位置: 首页 > 软件测试技术 > 单元测试 > 正文

如何更好地编写单元测试(上)

发表于:2017-08-06 作者:zhixin9001 来源:

  优秀的单元测试与糟糕的单元测试之间的区别是什么?如何写出完美的单元测试?这些问题并没有明确的答案。即使对于一个有十多年经验的优秀开发者来说,已经掌握的技能和形成的习惯并不能保证让他写出好的单元测试,因为单元测试与普通的开发实践有很大差别,而且大多数人在进行单元测试前已对单元测试的目的产生了一些无用甚至是错误的假设。
  至少在我看来,大多数单元测试是无用的。我没有责备开发者的意思,毕竟大多数情况下是公司要求做单元测试,然后他们就安装了NUnit并开始编写测试方法了,随后一边看着红红绿绿的标记,一边想着“我正在进行单元测试”。真正的单元测试并不是这样的。编写差的单元测试非常容易,但是这样编写的单元测试并不能为项目带来任何价值,相反,在需求变动的时候却可能造成“天文数量”的关联修改。那么,你现在的情况是不是这样的?
  一、单元测试的目的不是寻找BUG
  我非常推崇单元测试,但前提是我们需要正确理解单元测试在“测试驱动开发”(Test Driven Development [TDD])中扮演的角色,并排除自己脑海中关于“单元测试有助于排除BUG”的错误想法。
  经验告诉我,单元测试并不是发现BUG或是回归测试的有效方法。按照定义来看,单元测试是要对软件中的每个最小独立单元进行的测试。但是当你的软件在真实场景运行时,所有的独立单元都必须相互合作,这就导致软件整体的复杂度远远超过了单元测试用例数目的总和。能够证明X和Y模块能够独立工作,并不代表它们能与另外的模块合作无间;而且,用户反馈的问题往往看起来与单个模块的缺陷毫无关联;另外,你编写的测试用例并不能保证覆盖所有的情况,也就不能检测出所有可能发生的问题(例如,网络请求可能受到一些预料之外的干扰)。
  所以,如果你的目的是发现BUG,那么更有效的做法是模拟生产环境并把软件整个运行一遍,就像常规的人工测试那样。如果你构建了一系列测试用例并让它们自动运行来暴露将来可能发生的问题,这又属于集成测试的范畴了,集成测试与单元测试涉及到不同的技术。下面是一些关于不同测试方法的使用建议:

  注:关于单元测试,也会有一种例外的情况确实是为了发现BUG。比如在你重构某个模块的代码但并不打算改变该模块的原有行为时,使用单元测试可以保证模块的行为确实不会被改变。
  那么,既然单元测试不是用来发现BUG,它的作用到底是什么呢?
  关于这个问题的答案,我敢打赌你已经听说过不下上百次了,但由于我们大多数开发者脑中关于单元测试的错误观念相当顽固,我还是决定在这儿重复一下。就像TDD领域的专家说的:“TDD是一种开发流程,而不是一种测试流程”,我在这儿延伸一下:“TDD是一种交互式地设计健壮的软件模块或单元的方法,而且可以通过单元测试来验证这些模块或单元的行为是否符合预期”。
  二、优秀的单元测试和糟糕的单元测试
  TDD有助于让你交付符合预期的代码单元。优秀的单元测试具有极高的价值,它可以阐述你的设计、可以让重构变得容易,还可以让你在扩展代码之前对每个单元的行为有一个清晰的整体印象。
  相反,糟糕的单元测试则是相当有害的,它并不能清楚地阐述任何事情,却会妨碍你的重构工作,并会时不时得向你抛出错误。
  来看看在下面的坐标中,你的做法处于哪个位置?

  如果是按照TDD的流程创建的单元测试,那么应该与坐标轴最左边的情况相符。如果代码单元的行为改变,那么单元测试必须随之改变,反之亦然。但是这些单元测试与别的代码是毫不相关的,所以其它代码的更改不应该导致单元测试不通过(如果你的测试代码无法通过,说明你做的并不是真正的单元测试)。这样测试代码的维护成本就会很低,这也是TDD能够作为一种开发技术被应用于各种规模的项目中的原因。
  在坐标轴的另一端的是集成测试,集成测试不会关心代码单元层面的问题,它从用户的角度出发,考量的是系统整体的运作情况。集成测试的维护成本也是很低的,因为无论你怎样修改内部代码,最终展现给用户的功能是不应该变化的。
  如果你处在坐标轴的中间位置,说明你并不清楚自己在作何假设、在尝试证明什么。在这种情况,任何一处微小的单元代码的变动都可能迫使你去修改上百个看起来无关的单元测试用例,你因此会耗费大量的时间,有时甚至会达到你正常修改代码所需时间的10倍以上!此外,为了让这些互相耦合的测试通过,你需要添加更多的前置条件,但这样到头来实际上证明不了任何事情,处在这样一个恶性循环中真是一件令人沮丧的事情。