重构大型方法以确保我不会破坏任何内容时,会有什么帮助?


10

我目前正在重构大型代码库的一部分,而没有任何单元测试。我试图以残酷的方式重构代码,即试图猜测代码在做什么,什么更改不会改变它的含义,但是没有成功:它随机破坏了整个代码库的功能。

请注意,重构包括将旧版C#代码移动到更具功能性的样式(旧版代码未使用.NET Framework 3和更高版本的任何功能,包括LINQ),在代码可能会从中受益的地方添加了泛型,等等。

考虑到要花多少钱,我不能使用形式化方法

另一方面,我认为至少要严格遵循“任何重构的遗留代码都应带有单元测试”的规则,无论它要花多少钱。问题在于,当我重构500 LOC私有方法的一小部分时,添加单元测试似乎是一项艰巨的任务。

什么可以帮助我了解哪些单元测试与给定的代码相关?我猜测对代码进行静态分析会有所帮助,但是我可以使用哪些工具和技术:

  • 确切知道我应该创建什么单元测试,

  • 和/或知道我所做的更改是否以与现在不同的方式影响了原始代码?


您认为编写单元测试会增加此项目时间的原因是什么?许多支持者会不同意,但这也取决于您编写它们的能力。
JeffO

我并不是说这会增加项目的总时间。我想说的是,这将增加短期时间(即重构代码时我现在所花费的立即时间)。
2013年

1
formal methods in software development无论如何,您都不会使用它,因为它被用于使用谓词逻辑来证明程序的正确性,并且不具有重构大型代码库的适用性。通常用于证明代码的形式化方法可以在医疗应用程序等领域正常工作。没错,这样做的代价很高,这就是为什么它不经常使用的原因。
Mushy

出色的工具(例如ReSharper中的重构选项)使这项任务变得更加容易。在这样的情况下,它是非常值得的钱。
billy.bob 2013年

1
当不是所有其他重构技术都使我失败时,这不是一个完整的答案,而是一种愚蠢的技术,它出奇地有效:创建一个新类,将功能分解为具有正确代码的功能,仅每隔50行左右就分解一次,提升任何在各个函数之间共享给成员的局部变量,然后各个函数更好地适合我的头脑,并使我能够查看成员中哪些部分贯穿整个逻辑。这不是最终目标,只是使遗留的烂摊子准备好安全重构的一种安全方法。
Jimmy Hoffa 2013年

Answers:


12

我也遇到过类似的挑战。“ 使用旧版代码”一书是一个很好的参考资料,但是假设您可以在单元测试中费劲以支持您的工作。有时那是不可能的。

在我的考古工作中(我的维护此类旧代码的术语),我遵循与您概述的方法类似的方法。

  • 首先要对例程当前正在做的事情有一个深刻的了解。
  • 同时,确定例程应该执行的操作。许多人认为此项目符号与先前的项目符号相同,但是存在细微的差异。通常,如果例程正在执行其应做的事情,那么您就不会应用维护更改。
  • 在例程中运行一些示例,并确保击中边界情况,相关的错误路径以及主线路径。我的经验是附带损害(功能破坏)来自边界条件未完全相同地实施。
  • 在这些样本案例之后,确定哪些持久性不一定是必须持久的。再次,我发现正是这种副作用导致其他地方的附带损害。

此时,您应该具有该例程公开和/或操纵的内容的候选列表。其中一些操作可能是无意的。现在,我使用findstrIDE和IDE来了解其他哪些区域可以引用候选列表中的项目。我将花一些时间来了解这些引用的工作方式以及它们的性质。

最后,一旦使自己陷入混乱,以至于我理解了原始例程的影响,我将一次进行更改,然后重新运行上面概述的分析步骤,以验证更改是否按预期工作它工作。我特别尝试避免一次更改多个内容,因为当我尝试验证影响时,发现这会炸毁我。有时您可以进行多项更改,但如果我能一次遵循一条路线,那是我的偏爱。

简而言之,我的方法与您提出的方法类似。这是很多准备工作。然后做个谨慎的个人改变;然后验证,验证,验证。


2
+1仅用于“考古”。这是我用来描述此活动的相同术语,我认为这是一种很好的表达方式(也认为答案很好–我并不那么肤浅)
Erik Dietrich

10

重构大型方法以确保我不会破坏任何内容时,会有什么帮助?

简短答案:小步骤。

问题在于,当我重构500 LOC私有方法的一小部分时,添加单元测试似乎是一项艰巨的任务。

请考虑以下步骤:

  1. 将实现移到另一个(私有)函数中并委派调用。

    // old:
    private int ugly500loc(int parameters) {
        // 500 LOC here
    }
    
    // new:    
    private int ugly500loc_old(int parameters) {
        // 500 LOC here
    }
    
    private void ugly500loc(int parameters) {
        return ugly500loc_old(parameters);
    }
    
  2. 在所有输入和输出的原始函数中添加日志记录代码(确保日志记录不会失败)。

    private void ugly500loc(int parameters) {
        static int call_count = 0;
        int current = ++call_count;
        save_to_file(current, parameters);
        int result = ugly500loc_old(parameters);
        save_to_file(current, result); // result, any exceptions, etc.
        return result;
    }
    

    运行您的应用程序,并尽一切可能使用它(有效使用,无效使用,典型使用,非典型使用等)。

  3. 现在,您具有max(call_count)用于编写测试的输入和输出集;你可以写一个单一的测试,在所有的参数/结果集的迭代你并执行它们在一个循环中。您还可以编写一个运行特定组合的附加测试(用于快速检查特定I / O集合的传递)。

  4. // 500 LOC here回到您的ugly500loc功能(并删除日志记录功能)。

  5. 开始从大函数中提取函数(不执行其他任何操作,仅提取函数)并运行测试。此后,您应该具有更多的重构功能,而不是500LOC。

  6. 从此过上幸福的生活。


3

通常,单元测试是必经之路。

进行必要的测试,以证明电流能够按预期工作。慢慢来,最后的测试必须使您对输出充满信心。

什么可以帮助我了解哪些单元测试与给定的代码相关?

您在重构一段代码的过程中,必须确切知道它的作用和影响。因此,基本上,您需要测试所有受影响的区域。这将花费您很多时间...但这是任何重构过程的预期结果。

然后,您可以将所有内容拆散而没有任何问题。

AFAIK,没有防弹技术……您只需要有条不紊(无论您喜欢哪种方法),很多时间和很多耐心!:)

干杯,祝你好运!

亚历克斯


代码覆盖工具在这里至关重要。很难通过检查来确认您已通过大型复杂方法涵盖了所有路径。该工具可以共同显示Kitc​​henSinkMethodTest01()... KitchenSinkMethodTest17()覆盖第1-45、48-220、245-399和488-500行,但不要碰到它们之间的代码;将使您更轻松地找出需要哪些附加测试。
Dan在火光旁摆弄
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.