如何在单元测试 (junit) 中管理巨大的 类 和重构

How to manage huge sized classes and refactoring in unit testing (junit)

在我们的项目中,我们已经开发了数月的大型 classes 和子关系。另外,我们一直在开发junit测试用例。

自动化单元测试总的来说是好的,但在现实生活中并不像我们想象的那么容易。管理单元测试架构比创建模拟对象和存根等更容易。此外,我们正在测试 Dao 和服务层。

问题是我们的class属性太多了。 (我知道这不是一个好的面向对象设计,但它是遗留架构。)

例如;客户 class 有 58 个属性 并且它与地址、marsaccounts 等相关。总的来说,如果你想测试这个 class 你必须创建输入,输入 90个或更多属性。

我们的架构有很多关于客户的业务规则,因此我们必须创建超过 50 个客户输入来测试每个规则、方法或流程。

简而言之,您必须为所有人创建 4500 (90 x 50) 个属性,但为了可靠的测试(仅必要的属性)创建更少的属性。

准备测试输入既痛苦又烦人。 想象一下,向 Customer 对象添加了 2 列,它们存储关键值。这看起来很容易,但重构测试输入是毁灭性的。

如何管理测试存根以及如何克服庞大的输入集?

此致。

使用模拟客户。仅设置与您要测试的规则相关的属性。

In short, you have to create 4500 (90 x 50) attributes for a reliable test.

不,你不知道。你不是在想这个吧。 详尽 测试可能需要 4500 个属性,但可靠测试则不需要。

Mocking 是一种完全合法的 "real" - 没错 - 测试手段,这里是合适的答案。

有多种方法可以使您的代码和测试更易于管理。

customer class has 58 attributes and it's related to address, marsaccounts and etc.

单个 class 的属性太多了。您应该尝试重构 class 并将其分解为多个 class。你提到地址,如果你有许多与地址的不同字段相关的属性,你应该将它们重构到一个 Address 对象中。找到其他类似的相关字段并将它们拉出到自己的对象中将真正有助于减少输入的数量并使其更容易在测试中模拟。

Preparing the test inputs are painful and annoying. Imagine, 2 columns added to Customer object and they store critical values. it seems easy, but refactoring the test inputs are soul-destroying.

我建议创建一个 Customer TestDouble 进行测试。默认情况下,此 TestDouble 将被初始化为将所有 58+ 个属性设置为有效值。 TestDouble 还将为所有这些值设置设置器,因此可以为每个测试更改它们。

这样您的测试代码就不会重复设置所有不相关的字段,并使测试更能揭示意图。

例如:

@Test
public void invalidAccountIdShouldThrowBusinessRuleException(){

     CustomerTestDouble cust = new CustomerTestDouble();
     //only set what matters for this test
     cust.setMarsaccount( -1);

     BusinessRuleValidator validator = ...
     //wrap with whatever you do to check that validator throws Exception
     validator.validate(cust);

}

很明显会抛出异常,因为帐户 id 是 -1 没有其他混乱。

如果您以后必须向 Customer 添加新字段,(或重构它以使用新对象),您的大部分测试代码都不会改变。只有 CustomerTestDouble 和任何直接受影响的测试需要更改。

看起来您只有两个选择:减少相关属性或学习轻松设置具有大量属性的测试。

少属性:重构。没有人会为您提供更详细的帮助,因为我们不了解您的业务。尝试找到控制某些逻辑的较小的属性组,并仅使用这些属性测试该逻辑。

简单的测试设置:您可以使用客户生成器。默认情况下,他们会使用一些 standard/most 常用设置创建客户,您可以根据需要调整结果

customer = makeCustomer().withActiveStatus(false).withDebit(3000).build()

然后,当出现新属性时,您只需更改一处 makeCustomer()

您还可以使用参数化测试将测试用例定义为单行或从电子表格加载数据,这可能更易于维护。

通常当新属性出现时,它并不会完全改变一切。通常,当该属性不标准时,它只是在一个地方添加新行为。因此通常它只是将默认属性添加到构建器和一些覆盖该属性的测试

另一种简化测试的方法是 属性 测试(QuickCheck 系列),尽管在业务逻辑中使用它并不总是那么容易

如其他答案中所述,您最好的选择是将那些巨大的 类 重构为具有单一职责的更细粒度的 类,但为了让您的遗留代码接受测试可能值得研究测试数据 builder 模式。

本质上,测试数据生成器提供了一种隐藏对象构造的抽象。例如,它们可以为所有构造函数参数提供默认值,允许您在每个测试中指定相关参数(通常通过流畅的 API 使您的测试非常可读)。

new AddressBuilder()
    .withName("Sherlock Holmes")
    .withStreet("221b Baker Street")
    .withCity("London")
    .withPostCode("NW1", "3RX")
    .build();