重构遗留代码所需的建议

Advice needed for refactoring legacy code

我正在处理遗留代码库,我会使用 TDD 向我当前正在更改的代码添加新功能。

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

我有一个 Calculator class 具有以下实现:

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

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

此 class 继承自以下基础 class:

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

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

现在我想向计算器 class 添加一个 'Add' 函数。 这个函数看起来像:

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 基类的构造函数确实抛出异常,此单元测试将永远不会通过。

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

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

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

状态的东西

您可以:

  1. 将新方法创建为静态方法
  2. 创建一个临时替代构造函数,注释为 "Only for testing"
  3. 重构 class 以移除依赖项
  4. PowerMock
  5. 抑制它

但最安全的方法是创建脚手架测试。即使构造函数具有依赖性,这也会构建 class 。您可能需要使用 setter 来打破封装以进行测试。一旦 class 真正得到很好的测试,您就可以重构 class,添加更好的测试,最后删除脏脚手架测试。根据 class 的复杂性,这可能是适当的或矫枉过正。

您可以创建一个新的 protectedpackage-private 静态方法 add 以及额外的 Calculator参数(至少现在,直到可以轻松实例化 class),然后使用 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 的新 Class,并在 Calculator class(又名 composition), then redirect the arithmetic operations to it, so it will help for a better testing and it may promote a Single Responsibility Principle.