1、测试怎样才算优秀?
(1)测试代码的可读性和可维护性
(2)代码在项目中及特定源代码中的组织方式
(3)测试所检查的内容
(4)测试可靠性和可重复性
(5)测试对测试替身的使用
影响测试质量的因素是无穷尽的。同样,一些因素并非在各种情况下都起作用。对一些测试来说,执行速度可能是至关重要的,但对另一些来说,极度专注才是关键。测试代码的质量取决于观察者的眼睛,个人偏好关乎“优秀”的定义---我不忽略偏好的存在。
2、可读的代码才是可维护的代码
较差的可读性与缺陷密度密切相关。自动化测试是防止缺陷的有效保护。遗憾的是,自动化测试也是代码,其可读性也很容易变差。难以阅读的代码也就难以测试,导致更难为之编写测试。而且,我们编写的测试还远远达不到优秀的程度,因为我们需要围绕拙劣的结构、难懂的API调用及其非测试友好的结构来组织代码。
测试代码的可读性同样重要。测试代码难以理解,要多花时间才能明白测试的意图,如果测试出错后要调查代码来搞清情况。
3、结构有助于理解事物
不仅仅说结构是有用的,缺乏结果更是有害的。对于又长又臭的源代码,基本上没有人愿意碰他们。即使最简单的概念变化都难以映射到你面前的源代码上。没有结构可以让你的大脑依靠。无法分而治之---你不得不在脑海中处理整件事情,或者准备好用脑袋撞墙。
我们需要这样的结构---用与我们的大脑和心智相匹配的方式来分解事物。盲目的将代码外化为单独的源文件、类和方法,在一定时间内能减少代码的数量,从而降低大脑的负担。但是那并不足以隔离和理解我们感兴趣的程序逻辑。我们需要一个有意义的结构。
当面对庞然大物,一个明显的解决方案是切成碎片,将代码块抽取到方法中。但是如果分解庞然大物的边界不甚合理---如果它们没能映射到领域和抽象上---我们可能会适得其反,因为现在各个概念在物理上可能比之前更加分散,反而增加了在源文件之间来回切换的时间。重要的是代码结构是否有助于你快速而可靠的找到高层概念的代码实现所在。
假设你的应用程序被自动化测试很好的覆盖---但就一个自动化测试。这个测试只有一个巨大的测试方法,花半个小时执行应用程序的所有逻辑和行为。假设你做了一点调整,引入了bug,测试失败。这要花一段时间才能在测试代码中找到确切的出错位置。测试代码缺乏结构,无助于你理清相互的影响、某个对象是在哪里初始化的、出错时某个变量的值是多少等等。最终当你找到了错误,只能再次运行整个测试--整整30分钟---来确保你真的修复了问题,并且在这个过程没有再破坏其他东西。
假如倒退一个小时,你正要改动某一处。这次你学乖了,你要小心的确保自己理解了当前的实现,做了正确的改动。这时你会阅读代码,特别是精读测试代码,它会告诉你生产代码的预期行为。只是你找不到测试代码的相应部分,因为它缺乏结构。
你需要的是专注的测试,它可读、可达、可理解,这样才能:找到与手上任务相关的测试类;从那些类中识别出合适的测试方法;理解测试方法中对象的生命周期。
关注测试的机构并确保它有用,就可以做到以上几点。当然,具备有用的结构还不够。
4、如果测试了错误的东西就不好了
在找bug的过程中,尤其容易忽略的一个烦人细节是测试的内容。在挖掘代码时,我要做的头一件事情往往是运行所有测试,让它告诉我哪些正常,哪些不正常。有时候我过于相信测试名称,其实那些测试完全是在测试不同的东西。
这与良好的机构有关---如果测试的名字错误的表达了要测试的内容,那就像跟着错误的路标驾驶。用正确的方式测试正确的事物也很关键。从可维护的角度看,尤其重要的是,测试应该检查预期行为而不是具体实现。
5、独立的测试易于单独运行
人类---我们的大脑过于精确---是极其强大的信息处理器。我们几乎可以瞬间评估身体周围的环境并在眨眼间做出反应。我们能在意识到雪球飞过来之前就做闪避。这些反应根植于我们的DNA中。
当我们感知到相似模式时,行为图谱会指示身体移动。随着时间的推移,这些食谱慢慢变得成熟,我们很快就背负上了一个互联模式和行为的复杂网络。
这种情况也发生在工作中。首次探索别人的代码时,我们会在15分钟内形成对常见的惯例、模式、代码坏味道和陷阱的清晰图景。这是我们识别相似模式的能力在起作用,并且能够告诉我们可能还会在附近看到哪些其他东西。例如,一方面是方法的大小,如果方法过大,立马会明白在那些特定模块、组件或源文件中还有一大堆问题等着你;另一个信号是变量、类和方法的名字的描述性如何。
具体到测试代码,我会关注测试的独立水平。尤其是在架构边界附近。因为在边界上仔细发现了很多代码坏味道,于是学会了一看到外部依赖就特别小心,包括:时间,随机数,并发性,基础设施,现存数据,持久化,网络。这些事物的共同之处在于它们往往都很复杂。
隔离和独立很重要,因为没有它们就难以运行和维护测试。开发者为了运行测试而不得不对系统做的每件事都会使事情变得更加繁琐。(例如需要在文件特定位置创建一个目录,确保具备一个特定版本的MySQL运行在特定端口号上,或者添加一条用于测试用户登录的数据库用户记录,或设定一堆环境变量。这些小事增加了工作量,并会累积为奇怪的测试失败)
假如测试执行时的系统时钟或随机数生成器的下一个值,这些都不在你的控制之中,而这正是此类依赖的特征。作为经验法则,你想要避免由于这种依赖而导致测试古怪的失败。你希望将代码放进一个台钳,通过传入测试替身或者将代码与环境隔离,使其行为符合你的需要,从而控制一切。
在测试类中不要依赖测试的顺序。一般说来,不让测试互相依赖是指你不该让一个类中的测试依赖另一个类中测试的行为或结构。同样适用于同一个测试类中的依赖。
测试相互依赖会很脆弱(假设测试框架决定以不同顺序来调用测试方法?虚惊一场!假设JVM供应商决定改变反射API返回方法的顺序?虚惊一场!假如测试框架决定以字母顺序来运行测试?又是一场虚惊!)。
测试意外失败的最不寻常的例子是,一个测试作为套件的一部分时可以通过,而单独运行时却神秘的失败(反之亦然)。
那些症状散发着相互依赖的臭气。它们假设另一个测试在自己之前运行,而且那个测试会将系统置于某个特定状态。总之,当编写测试涉及时间、随机数、并发性、基础设施、持久化或网络时,应该特别小心。
作为经验来说,应该尽量避免依赖它们,将它们限制在小的隔离单元中,这样你的大部分测试就不会遭受并发症,也不用总是挨个处理它们---只有少数几个地方才用得着操心。该做以下:
第一,用测试替身替换掉第三方依赖库,根据需要将其包装到你自己的适配层中。将各种麻烦封装进适配层以后,你就可以独立测试其余的程序逻辑。
第二,将测试代码与其用到的资源放在一起,或许是在一个包内。
第三,让测试代码自己生产所需资源,而不是让他们与源代码分开。
第四,令测试自行建立所需上下文,而不要依赖于之前运行的测试。
第五,对于需要持久化的集成测试,那就使用内存数据库(干净的数据集,能极大简化测试的启动问题,并且通常启动的超级快)。
第六,将线程代码分为同步和异步两部分,所有程序逻辑都放在一个常规的同步代码单元中,就可以方便的进行测试并且没有并发症,将棘手的并发部分留给一小堆专用测试。
面对遗留代码时要做到测试隔离很难,哪些代码在设计时并未考虑可测试性,因此不具备你想要的模块化。但即使这样,仍然值得去打破哪些讨厌的依赖从而使得你的测试与环境隔离并且相互独立。
6、可靠的测试才是可靠的
快乐的测试,是指某个测试快乐的执行一段生产代码---或许是全部的执行路径---却没有一句断言。
几乎不会失败的测试等于废物。如果测试从不失败或者一直失败,那么就没价值。为了让测试值得依靠,就需要可重复。如果你的代码包含异步内容或依赖于当前时间,确保将它们隔离在一个接口之后,这样可以用测试替身来替换它们从而使测试可重复--这是测试变得可靠的一个关键要素。
7、每个行业都有其工具而测试也不例外
测试替身是程序员所熟知的stub桩、fake伪造对象、mock模拟对象的总称。它们本质上是为了测试目的、用于替换真实协作者的对象。它促进了改进:通过简化要执行的代码来加速执行测试;模拟难以出现的异常情况;观察那些对测试代码不可见的状态和交互。
测试框架是最基本的工具,可以编写自动化、可重复的测试。还有第三样工具---构建工具,无论构建过程是怎么样的,构建脚本中用到哪种工具和技术,都没有理由不将自动化测试集成到构建中。