【问题标题】:Advice needed for refactoring legacy code重构遗留代码所需的建议
【发布时间】:2020-04-02 11:34:47
【问题描述】:

我正在使用旧代码库,我想使用 TDD 为我当前正在更改的代码添加新功能。

请注意,当前代码库没有任何 UT。

我有一个具有以下实现的Calculator 类:

public final class Calculator extends CalculatorBase {
    public Calculator(Document document) throws Exception {
        super(document);
    }

    public int Multiply(int source, int factor) {
        return source * factor;
    }
}

该类继承自以下基类:

public class CalculatorBase {
    public CalculatorBase(Document document) throws Exception {
        throw new Exception("UNAVAILABLE IN UT CONTEXT.");
    }
}

注意:构造函数实际上做了很多事情,我不想在 UT 中做这些事情。 为简单起见,我让构造函数抛出异常。

现在我想在 Calculator 类中添加一个“添加”函数。 这个函数看起来像:

public int Add(int left, int right) {
    return left + right;
}

这段特定代码的 UT 应该非常简单。

@Test
@DisplayName("Ensure that adding numbers DOES work correctly.")
void addition() throws Exception {
    // ARRANGE.
    Calculator calculator = new Calculator(null);

    // ACT.
    int result = calculator.Add(1, 1);

    // ASSERT.
    Assertions.assertEquals(2, result);
}

由于CalculatorBase base 的构造函数确实抛出了异常,所以这个单元测试永远不会通过。

使这个可测试的棘手部分是CalculatorBase 类是由工具自动生成的,因此该类的源代码无法修改。

我应该采取什么(婴儿)步骤来确保可以测试Calculator 类上的Add 方法?目标是使整个项目可测试,甚至摆脱自动生成的东西,但我想尽可能使用 TDD 以便逐步重构代码。

有人可能会争辩说,我可以将Add 方法设为静态,因为它不使用Calculator 类的任何依赖项,但代码只是快速添加到一起。 Add 函数在实际场景中是消耗Calculator 类状态的其他东西。

【问题讨论】:

  • 我相信,顾名思义,测试驱动开发是一门用于开发软件的学科。您可以在没有 TDD 的情况下重构和单元测试您的代码。
  • 那么我该如何重构它,并确保我没有破坏任何东西?

标签: java code-cleanup legacy-code


【解决方案1】:

你可以:

  1. 将新方法创建为静态
  2. 创建一个临时替代构造函数,注释为“仅用于测试”
  3. 重构类以移除依赖项
  4. PowerMock 抑制它

但最安全的方法是创建一个脚手架测试。即使构造函数具有依赖关系,这也会构建类。您可能需要使用 setter 来打破封装以进行测试。一旦该类经过了很好的测试,您就可以重构该类,添加更好的测试并最终删除肮脏的脚手架测试。根据课程的复杂性,这可能是适当的,也可能是矫枉过正。

【讨论】:

  • 1.让我们假设 Add 方法确实使用了 Calculator 类的状态,因此将其设置为 static 将不起作用。 2. 临时构造函数应该添加到CalculatorBase 类中,但这是自动生成的。因此,一旦重新生成文件,构造函数就会再次被删除,并且 UT 将不再编译。 3. 在没有测试的情况下移除依赖是脆弱的。我可能会引入我不知道的错误。我不知道烫伤测试。你介意再解释一下吗?
  • 脚手架测试只是一种临时测试,用于在不引入新错误的情况下重构类。 (正如你所说,它很脆弱)。就像你改造一座旧建筑一样。你可能更喜欢“时间丑陋测试”这个名字这里有一些技巧:youtube.com/watch?v=1Z_h55jMe-M#t=1h1m7s
  • 所以更像是第一阶段的集成测试,只是为了使其可测试......
  • 您可能需要时间整合测试。记住 TDD != 单元测试
【解决方案2】:

您可以创建一个新的 protectedpackage-private 静态方法 add 并带有一个额外的 Calculator 参数(至少现在,直到类可以易于实例化),并使用Mockito 创建您的测试:

class Calculator extends CalculatorBase {

    private final int limit = 100; // to show that we need an instance state in add method 

    ....

    public int add(int left, int right) {
        return add(this, left, right);
    }

    static int add (Calculator calculator, int left, int right) {
        return Math.min(left + right, calculator.getLimit());
    }

    public int getLimit() {
        return limit;
    }
}

然后测试变成:

    @Test
    @DisplayName("Ensure that adding numbers DOES work correctly.")
    void addition() throws Exception {
        // ARRANGE.
        Calculator calculator = Mockito.mock(Calculator.class);

        Mockito.when(calculator.getLimit()).thenReturn(100);

        // ACT.
        int result = Calculator.add(calculator, 1, 1);

        // ASSERT.
        Assertions.assertEquals(3, result);
    }

但在这种情况下,我更喜欢使用简单的构造函数创建一个名为 ArithmeticCalculator 的新类,并在 Calculator 类(又名composition)中使用它,然后将算术运算重定向到它,所以它会有助于更好的测试,它可能会推广Single Responsibility Principle

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-09
    相关资源
    最近更新 更多