是否正在创建一个用于测试不良做法的私有构造函数?

时间:2016-11-07 19:36:58

标签: java unit-testing constructor

我遇到过一些java代码,其中公共构造函数使用一堆new运算符调用package-private构造函数来创建新对象。

public class Thing {

    //public
    public Thing(String param1, int paramm2) {
        this(param1, param2, new Dependency1(), new Dependency2());
    }

    //package-private for the sake of testing
    Thing(String param1, int param2, Dependency1 param3, Dependency2 param4) {
        this.memberVar1 = param1;
        this.memberVar2 = param2;
        this.memberVar3 = param3;
        this.memberVar4 = param4;
    }


    //...rest of class...
}

在我看来,这是错误的,因为您正在编写代码来测试它而不是编写正确的代码。我认为其他两个选项(我能想到的)是创建工厂或使用PowerMockito在适用的情况下注入新对象。就个人而言,我会写如下所示。

public class Thing {

    //public
    public Thing(String param1, int paramm2) {
        this.memberVar1 = param1;
        this.memberVar2 = param2;
        this.memberVar3 = new Dependency1();
        this.memberVar4 = new Dependency2();
    }

    //...rest of class...
}

实施此方法的最佳做法/正确方法是什么?

2 个答案:

答案 0 :(得分:3)

在任何已发布的内容中包含特定于测试的代码通常是不好的形式(但也有例外情况,所以不要过于仔细地阅读)。这有几个原因。

<强> 1。外部人员可能会以一种你从未想过的方式使用测试构造函数,因为他们要么没有读取文档,指出它用于测试,要么开发人员忘记记录它。

我们假设您想要写一些有用的Thing扩展名,但您无法在Thing的公共/受保护API中找到任何可以执行此操作的内容想要它。然后你会发现这个包私有构造函数似乎允许你想要的东西,但后来才发现它破坏了你的代码。你仍然无法做你想要的你浪费时间去探索那些没有成功的API。任何执行此操作的人都会对API持否定态度,并且不会将其推荐给其他任何人。

<强> 2。重构包名称会破坏内容。

由于Java默认可见性的工作方式,此测试代码对生产代码中发生的重构没有很大的弹性。如果测试代码位于同一个包中,则它只能调用该构造函数。如果重命名包,则调用它的测试代码将无法访问它,从而导致编译错误。当然,对于开发代码和测试的人来说,它是一个简单的解决方案,但即使没有添加这个小麻烦,重构已经不是很有趣。如果一群人以前能够成功地使用包私有东西来满足他们的需求,那么这就成了一个主要的问题 - 现在他们所有的代码都被破坏了。

在某些情况下,可以在测试和生产环境中运行难以编写的代码(例如,仅在应用程序联网时运行的功能)。在这些情况下,依赖注入可以成为你的朋友,但是如果可以避免更复杂的测试方案而不牺牲功能覆盖或在API中添加钩子而不想让其他开发人员看到它,那么更简单的测试总是最好的。

答案 1 :(得分:0)

我知道在StackExchange的其他地方对这个确切的话题进行了很好的讨论,但我现在对谷歌的好运并不强烈。我发现的副本不太有启发性。

就个人而言,我已经在生产中看到过三种类型的测试代码。

  1. 分支逻辑 - if (isTesting) foo() else bar();
  2. 测试方法 - Object foo();通过测试代码调用,但从未投入生产。
  3. 测试构造函数 - Foo(Object dependency1, Object dependency2)通过测试代码调用,但从未投入生产。
  4. 前两种类型在几个方面都有害。

    • 可读性受到影响,因为代码更长,更复杂。测试逻辑可以掩盖业务逻辑。
    • 可维护性受到影响,因为代码有更多的责任。更新测试可能需要更改生产代码。
    • 在最佳方案中,此测试代码是生产中的死代码。在最坏的情况下,客户端意外地在生产中执行此测试代码,从而导致不可预测的结果。

    第三种类型当然可能以上述相同的方式有害;但是,我相信所谓的测试构造函数可以减轻或消除生产中其他类型测试代码固有的问题。

    • 添加一个构造函数显然会延长代码(至少是略微)但不需要增加复杂性:如果构造函数是链接的,就像在这里的例子中一样,我们有一个实例化默认值,另一个只是分配字段。这种分离可能比单独的构造函数复杂得多。如果我们遵循普遍接受的 not 在我们的构造函数中实现业务逻辑的做法,那么我们应该没有什么风险来掩盖它。
    • 再次假设我们的测试&#34;构造函数被链接到生产构造函数并且除了赋值之外不实现逻辑,代码实际上没有向类添加任何责任:它仍然实例化并分配依赖项,但是作为单独的方法而不是一个。测试更改无法破坏我们的生产代码。
    • 根据概述的假设,我们的测试构造函数不是死代码:它实际上是在生产中从客户端调用的链接构造函数中调用的;因此,执行第二个构造函数的客户端(带有适当的参数)可以期望与链式构造函数相同的合同。

    在构造函数中调用new通常是不好的,因为依赖注入通常是好的。但是,在某些情况下,类的依赖关系的合理默认实现是显而易见的,并且在构造函数中实例化这些依赖关系会为客户端提供最简单的可能接口。在这些场景中,我发现链接第二个构造函数是合适的,它不会强制任何默认值,无论第二个构造函数是否有助于测试。

    作为奖励,链式构造函数实践鼓励编程到接口,因为第二个构造函数会让您考虑除默认值之外的依赖项实现。