重构遗留代码所需的建议

问题描述 投票:0回答:2

我正在使用旧版代码库,我将使用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类添加'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类是由工具自动生成的,因此无法修改该类的源代码。

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

有人可能会争辩说我可以使Add方法静态化,因为它不使用Calculator类的任何依赖关系,但是代码只是快速地添加在一起。在实际情况下,Add函数是其他确实消耗Calculator类状态的函数。

java code-cleanup legacy-code
2个回答
0
投票

您可以:

  1. 将新方法创建为静态方法
  2. 创建注释为“仅用于测试”的时间替代构造函数
  3. 重构类以删除依赖项

但是最安全的方法是创建脚手架测试。即使构造函数具有依赖关系,也可以构建类。您可能需要为了测试而用setter破坏封装。一旦对课程进行了充分的测试,您就可以重构课程,添加更好的测试,最后删除脏的脚手架测试。根据班级的复杂程度,这可能是适当的,也可能是过大的。


0
投票

如果我理解正确,那么这里的一个窍门是使用无害的构造函数将测试的类子类化:

public final class TestCalculator extends Calculator {
    public TestCalculator(Document document) {
       // No call to super!
       // You might need to replicate some logic here, e.g. setting dependencies
       // needed for code you want to test
    }

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

在此示例中,由于add方法是无状态的,什么也不做的构造函数可以很好地工作,但您的实际方法可能更复杂。此测试将通过:]

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

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

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

编辑:好的,因此您的评论清楚表明您的实际系统要复杂一些。让我们使该示例更为实际。我将从您的构造函数参数为Document中获得启发,并假设您的CalculatorBase记录了它所做的所有和的记录。我还假设您需要逻辑来设置字体,然后才能对其进行写入。因此您的基类如下所示:

public final class CalculatorBase  {
    private final Document document;

    public CalculatorBase(Document document) throws Exception {
       this.document = document;
       document.setFont('Times New Roman');
       // this symbolises all the logic you don't want
       throw new Exception("UNAVAILABLE IN UT CONTEXT.");
    }

    public add(int x, int y) {
        int result = x + y;
        document.write(x + " + " y + " = " + result);
        return result;
    }
}

因此,此基类没有默认的构造函数,现在您要测试的add方法期望文档存在字体集。

如果您真正使用文档并正确设置字体,则您的测试类将具有构造函数:

public TestCalculator(Document document) {
    this.document = document;
    document.setFont('Times New Roman'); // just remove the exception
}

[如果只关心告诉构造函数中传递的文档使用正确的参数来调用write方法,则可以使构造函数更简单:

public TestCalculator(Document document) {
    this.document = document;
}

并使用Mockito模拟来验证副作用:

import static org.mockito.Mockito.*;
@Test
@DisplayName("Ensure that adding numbers DOES work correctly.")
void addition() throws Exception {
    // ARRANGE.
    Document mockDocument = mock(Document.class)
    Calculator calculator = new TestCalculator(mockDocument);

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

    // ASSERT.
    Assertions.assertEquals(2, result);
    verify(mockDocument).write("1 + 1 = 2");
}
© www.soinside.com 2019 - 2024. All rights reserved.