当您有现有的测试以确保工作代码不会在此过程中被破坏时,重构是一种安全的操作。然而,许多组织在没有构建或维护相应测试的情况下积累遗留代码,并且在重构代码之前无法编写适当的测试。 DZone 的 2015 年软件质量调查结果报告称,61% 的受访者编写自动化测试的能力有限,因为他们有需要重写的遗留代码。在这种情况下,有两种选择:完全放弃冒险或勇敢地做事并修改代码。
如果让代码保持原样,您不仅会在持续维护方面产生成本,而且还必须将未来的维护成本添加到等式中。代码慢慢腐烂,未来的变更成本上升。
当项目无法再承担更多的技术债时,修改代码是唯一的选择。问题是为遗留代码编写测试很困难。根据您选择的语言(或者当前语言甚至不是您的选择),以下是您在尝试编写新测试时可能会遇到的一些问题:
-
无法创建测试对象。
-
对象不能与其依赖项分开。
-
有些单例创建一次并影响不同的测试场景。
-
有些算法不会通过公共接口公开。
-
测试代码继承了依赖关系。
是的,在遗留代码上编写新测试很有挑战性——但这并没有改变这样一个事实,即遗留代码通常是产品中最需要测试的区域。
换句话说,你正在进入丛林。
但在你这样做之前,你最好掌握足够的重构技术,这样当你遇到麻烦时,你就会知道该怎么做。其中一些技术是自动的,可以减少乏味和人为错误。其他的是手动的,因此会带来未知的风险。您需要将工具与手头的任务相匹配。
熟悉恶劣的环境
在开始移动代码之前,请熟悉周围的环境。第一步是阅读代码,从一个文件跳到另一个文件,并思考如何更好地组织项目。然后从重组源代码结构开始。将位于同一位置的类移动到单独的文件,将类型移动到您希望找到它们的区域,并修复拼写错误以提高代码库中工作的可读性和可操作性。结构是代码的模型,如果很难理解模型,就很难自信地进行重构。如果我们感到舒服,我们就会更有信心做出进一步的改变。
一切就绪后,开始重命名。类、函数、变量、文件——任何可以提高可读性的东西。如果程序员理解代码,测试会更有效(如果代码不能被理解,测试代码就没有多大意义)。
请务必修改不符合其真正用途/描述其功能的名称。例如,我们有一个名为“getValidCustomer”的函数,如果从数据库中更新了 Customer 对象,它会返回一个成功代码。将其重命名为“PopulateValidCustomer”是有意义的。 (当我们这样做时,将其更改为 void 方法。)现在名称描述了函数。
选择好名字并不像看起来那么简单。这是一门艺术,尤其是巨大的包罗万象的课程。如果我们使用更准确的名称,我们可以在心理上(和结构上)改进我们的模型。另一方面,使用通用名称会隐藏功能,这会导致不适当的功能像一个巨大的黑洞一样被吸引到这些类中。 (如果你曾经编写过“-Utils”类,你就会明白我的意思。如果你遇到这些类,请尝试将它们分开并正确重命名。)
重命名是低风险的,因为它主要由 IDE 自动完成。通常,在进行预测试重命名时,建议您更多地关注代码中的方法名称和变量。这些通常足够小,可以在不进行任何更大的、可能具有破坏性的更改的情况下进行修改。
穿透树叶
为了测试代码,您需要以不同的方式访问它:探测代码并检查结果;设置数据并查看代码如何反应;并用模拟对象替换依赖项以控制测试。可用的访问点越多,编写测试就越容易。设置和验证代码将更短,更不容易出错,并且能够获得更好的覆盖率。
我们可以更改代码以使用这些重构模式引入访问点:
-
更改可访问性: 将方法签名从私有更改为公共。
-
引入字段: 在做很多事情的长方法中,将测试数据存储在可访问的字段中。
-
添加访问器: 如果数据太隐蔽,使用“getters”和“setters”来探测和修改该数据。
-
引入接口: 如果我们想模拟一个依赖项,将其功能拆分为单独的接口并模拟您需要的特定接口。然后一旦测试开始就可以正确使用它。
-
虚拟化: 通过使函数虚拟化来启用覆盖和重新定义函数。
与重命名一样,这些更改也是低风险的。但是,您可能会遇到一些来自同行的抵制,他们会说,“我们不应该暴露它,这不是正确的设计。”向他们保证,出于测试设计的目的,这些暴露和修改是暂时的,一旦重构代码并构建新测试,就可以采用更复杂的方法。
排除障碍开辟道路
一旦您可以访问代码及其依赖项,就该实际移动代码了——这次不仅仅是为了提高可读性。通过分离和删除依赖关系,编写测试变得更简单。
有许多模式可用于从代码中删除依赖项:
-
移动方法: 尤其是在大类中,有明显不属于那个类的私有方法。当这些方法也使用依赖项时,可以将它们移到单独的类中。我通常在复杂方法中识别可提取的代码位,然后提取到该类中的私有方法。您还可以探索是否可以将这些方法移到其他类中。由此,我们得到两个好处:大类变小了,可以mock新方法而不是直接依赖。
-
提取类: 上面提到的方法也可以提取到全新的类中。一个额外的好处是你可以专门化新类,给它一个合适的名字,并确保它不会成为一个黑洞。
-
引入参数: 当方法直接使用依赖项(可能不止一个依赖项)时,应该修改此过程,以便将依赖项从调用代码发送到方法。这样您就可以从测试中设置依赖关系。例如,如果一个函数调用一个静态方法,您可以引入一个参数,该参数将包含该静态方法调用的结果。这不仅消除了依赖关系并使代码可测试,还将依赖关系调用移到了链中。通过这样做,您可以将 Extract Class 用于测试代码,并从关注点分离中受益。
在这些步骤中四处移动代码确实会增加影响系统功能的风险。成对执行的缓慢、深思熟虑的修改将有助于避免破损。幸运的是,其中一些修改可以由 IDE 自动完成,从而降低了这种风险。
冒险开始
我们的旅程始于丛林,希望修改遗留代码以获得可测试性的好处。事实上,这些技术也适用于一般重构——将代码修改为更简单、模块化和更具可读性。
不过,团队通常不会就此止步。清理完一条路后,真正的乐趣就开始了。您可以分离更多类、从循环中提取逻辑、反转条件以及进行其他高风险修改。随着代码的简化,测试将变得更容易和更有效。
但夕阳西下,你需要安营扎寨。你必须自己继续这个旅程。有大量 资源专门 用于此主题,所以不要在这里停下来。
有关编写、测试和监控质量代码的最佳实践,请获取免费的 DZone 代码质量和软件敏捷性指南!