【问题标题】:How Dependency Injection Fosters Testability依赖注入如何促进可测试性
【发布时间】:2011-12-20 15:50:07
【问题描述】:

我一直在阅读Factory 模式,并且看到一些文章建议将工厂模式与依赖注入结合使用,以最大限度地提高可重用性和可测试性。虽然我还没有找到这种 Factory-DI 混合的具体示例,但我将尝试并给出一些我的解释的代码示例。但是,我的问题实际上是关于这种方法如何提高可测试性。

我的解释:

所以我们有一个Widget 类:

public class Widget {
    // blah
}

我们想要包含一个WidgetFactory 来控制Widgets 的构造:

public interface WidgetFactory {

    public abstract static Widget getWidget();
}

public class StandardWidgetFactory implements WidgetFactory {

    @Override
    public final static Widget getWidget() {
        // Creates normal Widgets
    }
}

public class TestWidgetFactory implements WidgetFactory {

    @Override
    public final static Widget getWidget() {
        // Creates test/mock Widgets for unit testing purposes
    }
}

虽然这个例子使用了 Spring DI(这是我唯一使用过的 API),但我们谈论的是 Guice 还是任何其他 IoC 框架并不重要;这里的想法是,我们现在要在运行时注入正确的WidgetFactory 实现,这取决于我们是在测试代码还是正常运行。在 Spring 中,bean 配置可能如下所示:

<bean id="widget-factory" class="org.me.myproject.StandardWidgetFactory"/>
<bean id="test-widget-factory" class="org.me.myproject.TestWidgetFactory"/>

<bean id="injected-factory" ref="${valueWillBeStdOrTestDependingOnEnvProp}"/>

然后,在代码中:

WidgetFactory wf = applicationContext.getBean("injected-factory");
Widget w = wf.getWidget();

这样,一个环境(部署级)变量(可能在某处的 .properties 文件中定义)决定 Spring DI 是注入 StandardWidgetFactory 还是 TestWidgetFactory

我做得对吗?!? 这似乎是很多基础设施为我的Widget 获得了良好的可测试性。并不是说我反对它,但对我来说,这就像过度设计。

我的挂断:

我之所以问这个问题是因为我将在其他包中拥有其他对象,这些包中有使用 Widget 对象的方法。也许是这样的:

public class Fizz {
    public void doSomething() {

        WidgetFactory wf = applicationContext.getBean("injected-factory");
        Widget widget = wf.getWidget();

        int foo = widget.calculatePremable(this.rippleFactor);

        doSomethingElse(foo);
    }
}

如果没有这个庞大的、看似过度设计的设置,我将无法将“模拟Widgets”注入到我的Fizz::doSomething() 单元测试中。

所以我很伤心:一方面,我觉得我在想太多事情——我很可能正在做这件事(如果我的解释不正确)。另一方面,我没有看到任何干净的方法来绕过它。

作为一个切线问题的延续,这也引起了我的另一个巨大担忧:如果我的解释是正确的(甚至有些正确),那么这是否意味着我们需要 Factories 来处理每个对象?!?

这听起来像过度工程!什么是截止?什么时候使用工厂,什么时候不使用?!感谢您对冗长的问题的任何帮助和道歉。简直让我头晕目眩。

【问题讨论】:

  • 注意:不能声明抽象静态方法或覆盖静态方法。

标签: java unit-testing dependency-injection junit factory-pattern


【解决方案1】:

DI/IoC 有助于测试,因为您可以轻松地决定使用什么实现,而无需修改使用它的代码。这意味着您可以注入一个已知的实现来执行特定的功能,例如,模拟 Web 服务故障、保证功能的好(或坏)输入等。

制造 DI/IoC 不需要工厂。是否需要工厂完全取决于使用细节。

public class Fizz {

    @Inject    // Guice, new JEE, etc. or
    @Autowired // Spring, or
    private Widget widget;

    public void doSomething() {
        int foo = widget.calculatePremable(this.rippleFactor);
        doSomethingElse(foo);
    }

}

【讨论】:

  • 谢谢!您是否有机会修改您的答案以表明我是否正确设置了 DI,和/或向我展示如何编写 Fizz::doSomething() 而无需 WidgetFactory 的示例?
  • 如果我在不同的Fizz 方法中需要不同的Widgets 怎么办?我假设在这种情况下注入WidgetFactory 是合适的?
  • @AdamTannon 可能,虽然不知道实际用例很难说。
【解决方案2】:

在您的 Fizz 示例类中,显式调用 DI 框架以获取您想要的 bean 是一种 DI 反模式。 DI 的全部意义在于好莱坞原则(不要打电话给我们,我们会打电话给你)。所以你的 DI 框架应该在 Fizz 中注入一个 Widget。

说到测试,我的偏好是

  • 测试夹具创建依赖项的存根或模拟。或者在适当的时候使用真实的东西。
  • 测试夹具使用我的 DI 框架在生产中使用的相同构造函数或设置方法注入存根。

有些人喜欢在他们的 DI 容器中运行测试,但我看不出这对单元测试有什么意义——它只是创建了额外的大量配置来维护。 (对于集成测试,这是值得的,但您仍然应该在生产环境和集成测试环境之间拥有尽可能多的 DI 上下文。)

所有这一切的结论是,我没有看到工厂模式有很多用处。

您能给我们一个您正在阅读的文章的链接吗?我看看能不能自己挖一两个。

编辑:这是承诺的链接:Is Dependency Injection Replacing the Factory Patterns?,这是关于 DI 的一系列相当不错的文章中的一篇。

【讨论】:

  • 谢谢 Andrew - 但这是否意味着我需要在 Fizz 中创建一个 Widget 成员(以便我可以将其设置为属性)?如果是这种情况,那么我需要为每个 Fizz 方法中使用的每个类创建实例变量......那里感觉不对。我最终可能会得到 Fizz 有 30 个实例变量!
  • DI 不是工厂的替代品;它们解决了不同的问题——在您链接的文章中标记为“DI 如何解决这个问题”的部分中,仍然有一个对象工厂(并且被明确称为)。 DI 可以解决工厂的一些用途。
  • @AdamTannon - 如果你保持你的类很小并且专门用于特定目的,你就不会遇到实例变量过多的问题。
  • @AdamTannon DI 的全部意义在于让容器为您完成提升。但是是的,理想情况下,在松散耦合的系统中,将注入实例变量,从而减轻类的实例化责任。
  • @AdamTannon 如果你想以通常的 DI 方式注入它,是的。虽然如果 Fizz 使用 30 个其他类,它可能有太多的责任,需要拆分。
【解决方案3】:

我认为这个问题的核心答案很简单:依赖注入的好处可以使用普通的 java 来实现,但代价是添加了大量的样板。

DI 允许您解耦和模块化您的类,而不必在您的源代码库中添加一堆参数化的构造函数/工厂方法等。

【讨论】:

    【解决方案4】:

    依赖注入确实让代码更易测试。当我谈到使用依赖注入的未经测试的 EJB3 代码时,与测试其他类型的遗留代码相比,这就像白天和黑夜。

    对于工厂,它们可以有两个角色。当您没有依赖注入框架时,一种是依赖注入。这是一个静态工厂模式,在 effective Java 中谈到,这实际上是 Java 独有的,最好是出于其他原因(在那里详细说明)。您在 JDK 中看到了其中的一些内容,它是测试的熊,当然不会自然生成可测试的代码,因为您必须非常注意使用它。

    工厂的另一个角色,在依赖注入中使用四人组的真实模式,是在某种程度上涉及实际对象的构造,而不是简单的构造函数。在这种情况下,你注入一个工厂,它可以用一个简单的接口表示,然后让工厂实现完成繁重的工作。这更多是围绕着编程的原则构建的,没有问题是再一层间接解决不了的。

    实际上直到我开始做 Guice 依赖注入,我才发现 Java 需要 GOF 工厂模式。

    【讨论】:

      【解决方案5】:

      DI 使您无需使用工厂即可对接口进行编程。对接口进行编程以及在类之外传递具体实现形式的能力使代码更易于测试。

      interface Widget {
      }
      
      class UserOfWidget {
         Widget widget;
         public UserOfWidget(Widget widget) {
            this.widget = widget
         }
      }
      
      Using spring dependency injection
      <bean id="widget" class="ConcreteWidget"/>
      <bean class="UserOfWidget">
         <constructor-arg ref="widget"/>
      </bean>
      
      In a test case
      UserOfWidget uow = new UserOfWidget(new StubWidget()); 
      

      如果没有 DI 来实现同样的效果,您需要将具体 Widget 的创建隐藏在工厂后面 - 类似于您在示例中的凌乱代码。

      也就是说,即使在使用 DI 时也有使用工厂的情况。主要用于对象构造复杂的场景 - 需要多个步骤,不能仅通过简单的构造函数调用来完成。一个这样的示例是从 JNDI(例如数据源)中检索对象。从 JNDI 中查找对象的所有逻辑都封装在工厂中 - 然后可以将其配置为 XML 文件中的简单 bean。

      总而言之,您不需要使用 DI 的工厂来使代码更具可测试性。只需 DI 就足够了。工厂只需要简化创建复杂的对象的创建。

      【讨论】:

        【解决方案6】:

        你有一个工厂,但你没有在你的 Fizz 类中使用依赖注入。这就是为什么您没有轻松测试它的原因。您需要直接注入 applicationContext 或 WidgetFactory。在大多数情况下,我更喜欢后者。

        public class Fizz {
            private WidgetFactory wf;
            public Fizz(WidgetFactory widgetFactory) {
                wf = widgetFactory;
            }
        
            public void doSomething() {
        
                Widget widget = wf.getWidget();
        
                int foo = widget.calculatePremable(this.rippleFactor);
        
                doSomethingElse(foo);
            }
        }
        

        现在你有了这个,你可以将模拟对象注入你的 Fizz 类。在下面的示例中,我使用了 Mockito,但您可以使用其他模拟框架(或者如果您真的需要,也可以制作自己的模拟)。

        @Test
        public void test() {
            WidgetFactory wf = mock(WidgetFactory.class); // mock is a Mockito function that creates mocks for you automatically.
            Fizz objectUnderTest = new Fizz(wf);
        
            // Test Fizz
        }
        

        在您的单元测试中,您确实不想加载 applicationContext。这就是为什么你在代码中模拟它然后直接传递它。因此,您将只有一个简单的 applicationContext 和用于生产的。

        <bean id="widget-factory" class="org.me.myproject.StandardWidgetFactory"/>
        <bean id="fizz" class="org.me.myproject.Fizz">
            <constructor-arg ref="widget-factory"/>
        </bean>
        

        关于你关于需要工厂来做所有事情的问题,不,你不需要工厂来做所有事情。对于我的类使用和不创建的对象,我更喜欢使用 DI 并通过构造函数将其传递。如果它是可选的,您也可以通过 setter 传递它。如果该类确实创建了一个新对象,那么工厂可能是合适的。当您开始学习如何使用 DI、工厂和编写可测试的代码时,我想您将能够感觉到编写工厂是否太多。

        【讨论】:

        • David - 感谢您非常彻底的回答和有用的代码示例!你帮助我在理解这些东西方面迈出了下一步。我确实有与@Andrew Spencer 相同的问题:这种安排强制我为我想要注入的每个对象在Fizz 的设计中添加一个实例变量。因此,这意味着如果Fizz 有 10 个方法,并且在这 10 个方法中的每一个方法中总共有 15 个不同的对象(在其他包的其他地方定义),那么我需要为每个对象有 15 个实例变量!这是正确的还是我错过了什么?!?
        • 是的,如果您的班级中有 15 个依赖项,那么这就是您想要考虑的事情 - 它真的需要所有这些东西来完成它的工作吗?每当我看到一个包含多个注入服务的类时,我都会尝试找出一种重构方法
        • @AdamTannon - 我在 Andres Spencer 帖子中的 cmets 中提到,如果您将班级缩小并赋予他们更具体的目的,您将不会遇到这个问题。无论如何,目的有限的小类更易于重用和测试,所以我一般鼓励它。您可以重构 Fizz,或者您可以尝试首先在较小的类上应用 DI(有或没有工厂)。这将帮助您更好地理解这些概念。
        猜你喜欢
        • 1970-01-01
        • 2013-05-23
        • 2011-02-28
        • 2018-01-27
        • 1970-01-01
        • 1970-01-01
        • 2021-06-19
        • 1970-01-01
        相关资源
        最近更新 更多