【问题标题】:How good is it to use additional constructors just for testibility?仅仅为了可测试性而使用额外的构造函数有多好?
【发布时间】:2016-08-12 14:41:56
【问题描述】:

目前我需要写SecondClass,并决定使用这种解决方案来写,否则它就无法测试。

@Component
public class FirstClass {

    public void doStuff() {
        System.out.println("First Class stuff!");
    }

}

@Component
public class SecondClass {

    private final Random random;
    private final FirstClass firstClass;

    @Autowired
    public SecondClass(FirstClass firstClass) {
        this(new Random(), firstClass);
    }

    public SecondClass(Random random, FirstClass firstClass) {
        this.random = random;
        this.firstClass = firstClass;
    }

    public void doOtherStuff() {
        firstClass.doStuff();
        System.out.println("Second Class stuff " + random.nextInt(10));
    }

}

我的同事不喜欢我的解决方法,他们更喜欢这样实现SecondClass

@Component
public class SecondClass {

    private Random random;
    private final FirstClass firstClass;

    @Autowired
    public SecondClass(FirstClass firstClass) {
        this.random = new Random();
        this.firstClass = firstClass;
    }

    public void doOtherStuff() {
        firstClass.doStuff();
        System.out.println("Second Class stuff " + random.nextInt(10));
    }

    public void setRandom(Random random) {
        this.random = random;
    }

}

我不同意这种解决方案,因为我认为 Random 是此类的必要部分,它不会在运行时更改,并且 setter 仅用于测试目的,这就是为什么我更喜欢两个解决方案构造函数。

我们也想出了这种构造函数:

@Autowired(required = false)
public SecondClass(FirstClasss firstClass, Random random) {
    this.random = (random == null ? new Random() : random)
    ...
}

但实际上在构造函数中注入了更多组件,因此如果所有组件都需要,那将是更可取的。

我想知道这里有没有人有过这种经历?您如何看待这个案例以及是否有更好的方法来解决这个问题?

【问题讨论】:

  • 在我的工作经验中,我使用了很多多个构造函数,并且final 字段是强制性的(显然有理由这样做)
  • @PaoloForgia 您能否告诉我们更多有关您遇到问题的经历以及这种多个构造函数何时真正有帮助的经历。
  • 这在我看来像是 XY 问题 (xyproblem.info)。您只想添加第二个构造函数吗?或者您是否试图通过添加此构造函数来解决其他问题?也许您想测试SecondClass 并且您需要Random 生成一些常量值以便能够在测试中处理它们?

标签: java spring oop spring-boot


【解决方案1】:

您的问题源于拥有 final 字段。关注鲍勃叔叔,随时保护它们,以便您可以在测试课程中给它们写信。您不是在编写接口,而是在编写代码,因此访问修饰符并不那么重要,并且 protected 很好,因为它允许包访问。

或者您可以通过硬连线将它们自己设置在您的测试用例中,例如ReflectionUtils.setField

【讨论】:

  • 是的,我们也考虑过反射,但在我看来,它只是绕过了这个问题,没有解决它。我认为它与类中的附加设置器几乎相同。
  • 我从来没有见过任何人在测试类中遇到反射问题(当然,除非你过度使用它,但是......)你的问题是你想要在你的类中随机,屏蔽掉。商业上,如果这是最好的决定,那就应该是这样。测试是次要问题,仅针对测试用例修改代码不一定是坏事,但如果您可以通过一些轻微的黑客攻击/反思来防止它,大多数人根本没有问题。
  • 不,我并不是说我会遇到反射或任何其他解决方案的问题,我只是对解决此类问题的“按书本”方式感兴趣。因为我总是被警告说,在没有依赖注入的可能性的情况下在类中创建新对象是个坏主意,但这只是这种事情发生在实际项目中的说法,如果我想保持良好的 oo 架构,我应该采取行动。
  • 好吧,如果你觉得你的测试不应该使用反射(为什么不呢?性能不是问题?安全黑客也不是,很明显?),你只是剩下修改您的“公共界面”。然后,对于那些使用该界面的人来说,什么是最清楚的问题就变成了一个问题。它是一个额外的构造函数参数,一个单独的设置器,还是让你的测试可以访问一个字段。一切都“还好”,尽管我倾向于至少有点远离公众视线的人 - 通过将其设为 protected 来让您的领域易于访问。您必须在两个 guidelines 而非 rules 之间进行选择
  • -1 因为在精心设计的抽象中,受保护的非最终字段从来都不是必需的,因为它们的封装很差。根据open/closed principle,非最终实例字段必须始终是private,以便抽象控制是否应该修改它们以及如何修改。
【解决方案2】:

如果您在SecondClass 中声明了两个构造函数,并且仅在单元测试中使用其中一个,则意味着单元测试的构造函数可以替换为您的单元测试的更相关的技巧,例如反射到设置random 字段的值。
在可以避免的情况下,您不应该仅仅为了单元测试而打开 API。
当您别无选择时,您应该只为单元测试打开您的 API。

如果random 字段是只读的,并且在SecondClass 被实例化后即为已知值,则您应该有一个SecondClass 的构造函数。并且random 字段应该是final 并且在SecondClass 的构造函数中赋值。

【讨论】:

  • 我没有从我的问题中更改该术语RandomdoTheStuff 方法中确实是必要的,是的,它是只读的,除了doTheStuff 方法之外,它不需要在其他任何地方访问它。我不喜欢在单个构造函数中实例化随机,只是因为我认为这违反了 OOP 原则,如果我在构造函数类中实例化它只会失去灵活性。但这只是我的看法。
  • -1 因为单元测试代表真正的客户端代码可能会做的事情。因此,如果单元测试需要构造函数,它实际上对真正的客户端代码很有用。因此,反射不是单元测试的好选择:单元测试应该检查公共测试的 API 是否完整且设计良好;如果需要反射来检查它,那么 API 是不完整的。
  • 对于无状态类来说完全正确。这完全不是给定的问题。结果不是由客户端决定的,而是由状态客户端决定的。这并不意味着状态是public,并且应该是可修改的。要测试固有状态的可变性,您需要控制该状态。这意味着要么通过拥有访问它,要么通过反射获得对它的访问。但是对于有状态的对象,通常那个状态是关闭的,所以访问应该是不可能的。
  • @Koos 我基本同意。但是公共性不在于状态本身,而在于对状态的操作:组件可能对读取其状态具有公共访问权限,而无权对其进行写入,没关系。实际上,状态应该总是关闭才能修改,让设计者决定是否应该打开阅读。
  • @LittleSanti 但是你的设计是为了测试,还是为了安全?两者都有不同的要求。这里的问题正是关于这一点。您是否允许修改状态(通过构造函数、设置器)以便可以进行适当的测试,或者您是否允许关闭状态,只打开以供阅读,从而保持安全性?我个人不明白这两个要求如何协调和解决。
【解决方案3】:

你说得对,我的朋友。您的方法是正确的,因为根据面向对象的设计,对象必须在其任何构造函数执行后初始化为有效状态。因此,如果需要random 字段,必须在所有构造函数中初始化。此外,如果以后不更改,它必须是最终的(并且没有设置器)。所以第二种方案是不对的,因为它违反了这个原则。

我也不喜欢第三种解决方案,因为它对客户端代码来说可能是愚蠢的:虽然客户端假设 null 被设置为输入值,但实际上使用了另一个(不受控制的)值.

因此,如果您需要 SecondClass 可测试的构造函数,请添加它。因为如果一个类是不可测试的,它也是不可执行的。测试是确保类可用的正确方法。

【讨论】:

  • 我不同意。单元测试并不代表客户端对应用程序所做的事情。它以孤立的方式测试组件的单一行为。但是如果你以不同的方式对它的组件进行单元测试,它们被客户端使用,你就是在作弊,你测试的代码并不能反映真实的行为。此外,如果不用于生产环境,我们不应该只为单元测试而开放 API。这是没有意义的。
  • “单元测试不代表客户端对应用程序做了什么”???那么,如何确保您的组件有用并按预期工作呢?
  • 我确认。集成测试代表客户端对应用程序的操作。您混合了不在同一级别的不同概念:组件和应用程序。
  • 请注意,我总是指客户 code 应该做什么。我不是在谈论“人类”客户或用户。也许你误解了我的意思。不过,您还没有回答我的问题:您如何确保组件的行为符合预期?
  • 我说的是客户端,而不是客户端类或客户端代码。您当然可以对代码进行单元测试。您可以阅读我的答案来理解它。它是平衡的。 “当你可以避免的时候,你不应该只为了单元测试而打开你的 API。当你别无选择时,你应该只为了单元测试而打开你的 API。”一切都不是白色或黑色。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-11-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-01-09
  • 1970-01-01
相关资源
最近更新 更多