修改代码的艺术----- 2.2 高层测试 2.3 测试覆盖
2.2 高层测试
单元测试的确很棒,但高层测试也有其一席之地。所谓高层测试便是那些覆盖了某个应用中的场景和交互的测试。高层测试可以用来一下子就确定一组类的行为。能够这样做往往就意味着你可以更容易地为单个类编写测试。
2.3 测试覆盖
那么在一个遗留项目中我们究竟该如何着手进行修改 呢?我们首先注意到,如果可以选择的话,在进行修改时有测试“罩着”总是要安全一些的。对代码的修改可能会引入bug,因为我们毕竟是人而不是神。但假如 在修改代码之前先用测试将代码“护住”,我们就能更容易地捕获到在改动过程中所犯的错误了。
图2-1展示了一小组类。我们想要改动InvoiceUpdateResponder的getResponseText方法以及Invoice的getValue方法。这两个方法是我们的改动点,我们可以通过为它们所在的类编写测试来覆盖它们。
图2-1 发票更新类
要想编写并运行测试,我们先要能够在一个测试用具中 创建InvoiceUpdateResponder和Invoice的实例。那么我们能否做到这一点呢?看起来创建Invoice的实例可以不费吹灰之力 地完成,因为它有一个无参的构造函数。而InvoiceUpdateResponder的情况则可能要复杂一些,它的构造函数接受一个 DBConnection,参数这是一个数据库连接,必须真正连接到一个实实在在的数据库。问题来了,在测试中我们该怎样处理这种情况呢?我们是否要为此 建立一个数据库,并在其中存上测试所需的数据呢?这么做的工作量可不小。而且有没有考虑到涉及数据库的测试会比较慢呢?无论如何,我们在做这个测试的时候 对数据库并不特别关心,只是想覆盖我们对InvoiceUpdateResponder和Invoice的改动。此外还有一个更大的问题,即 InvoiceUp- dateResponder的构造函数需要一个InvoiceUpdateServlet作为参数。创建一个这样的对象可想而知有多麻烦。当然我们可以改 动一下InvoiceUpdateResponder的代码,让它不再接受InvoiceUpdateServlet为参数。如果 InvoiceUpdateResponder只是需要从InvoiceUp- dateServlet那里获取很少一点信息的话,我们就可以将这些信息传给它,而不是传给它整个servlet,然而,在做上述改动的时候我们是不是也 需要做个测试来确保改动的正确性呢?
以上这些问题都属于依赖问题。当一个类直接依赖于某些难以在测试中使用的东西时,这个类就是难以修改和处理的。
依赖性是软件开发中最为关键的问题之一。在处理遗留代码的过程中很大一部分工作都是围绕着“解除依赖性以便使改动变得更容易”这个目标来进行的。
那么,我们具体又该怎么做呢?在不改变代码的前提下 我们如何才能将测试安置到位呢?不幸的是,在许多情况下想要做到这一点是不大容易的,某些时候甚至根本不可能。就在我们刚刚看到的这个例子中,我们当然可 以通过使用一个真实的数据库来解决DBConnection问题,然而servlet问题又该怎么办呢?我们是不是也要创建一个完整的servlet对象 并将它传递给InvoiceUpdateResponder的构造函数呢?我们能否将这个servlet设置到正确的状态呢?可能吧。但假如我们将要测试 的是一个图形用户界面的桌面应用程序又该怎么办呢?这样一个应用程序也许并没有任何可供我们测试时利用的可编程接口,其逻辑可能被捆绑在GUI类当中。这 时候我们该怎么办呢?
遗留代码的困境
我们在修改代码时,应当有测试在周围“护”着。而为了将这些测试安置妥当,我们往往又得先去修改代码。
在上面的Invoice例子当中,我们可以试着在一 个更高的层别来进行测试。如果对于某个特定的类来说,不改变它就难以为它编写测试的话,那么转而去测试使用它的那些类往往会简单一些。然而不管怎么样,我 们通常最终还是免不了要在某个点上解开类之间的依赖。在当前的这个例子中,我们可以解开InvoiceUpdateResponder对 InvoiceUpdateServlet的依赖:只需将InvoiceUpdateResponder真正需要的东西传给它就行。 InvoiceUpdateResponder需要的是InvoiceUpdateServlet所持有的一组发票ID。同样,我们也可以解开 InvoiceUpdate- Responder对于DBConnection的依赖:只需引入一个接口(IDBConnection)并将Invoice- UpdateResponder改为使用该接口即可。图2-2展示了这些类在上述改动之后的样子和关系。
那么,在没有测试保护的情况下进行上述的重构到底安 不安全呢?实际上它们可以是安全的。上述的用于解开InvoiceUpdateResponder对InvoiceUpdateServlet和对 DBConnection的依赖的两种重构手法分别称作朴素化参数(Primitivize Parameter,302页)和接口提取(Extract Interface,285页)。在本书的最后,解依赖的技术一部分中对它们有详细描述。在解依赖时,我们通常可以采用编写测试的手段来让较具侵入性的修 改更为安全。诀窍就在于要非常保守地进行上述最初的重构。
图2-2 解除依赖后的发票更新类
当我们的改动可能会引入错误的时候, 保守地进行改动就成了不二之选,然而有时候(为了让测试覆盖代码而解依赖)结果代码却并不像前面的例子中那样光鲜漂亮,例如我们可能只是为了能够将测试安 置到位而为某个方法引入了某个在产品代码中并不严格需要的形参,或者以古怪的方式将某个类分裂开了。当这么做的时候,我们可能最终会令代码看上去稍微糟糕 一些。另一方面,如果不那么保守,则我们可以立即解决这个问题。但话虽如此,具体还要看这么做会带来多大的风险。如果错误是(它们的确通常是)个重要的考 虑因素的话,保守改动常常是有好处的。
当 在遗留代码中解依赖时,你常常不得不暂时将自己的审美感放在一旁。有些依赖能够干净利落地解除,而有些从设计的角度来看最终还是解决得不那么完满。这就好 像做手术总要有一个刀口一样,刀口在缝合之后可能会变成一道疤痕,你的改动也可能会在代码中留下“疤痕”,然而“疤痕”之下的东西则已得到了治愈。
而且,如果以后能够用测试覆盖“疤痕”四周(即你当初解依赖的点)的话,你就可以将“疤痕”也抹掉了。