【问题标题】:Working with Injectable Singletons使用可注入单例
【发布时间】:2012-08-13 11:54:46
【问题描述】:

我最近偶然发现了这个interesting concept,它可以为我节省很多测试工作。 我不明白的是如何在运行时注入提供程序?

场景很简单:我正在使用我选择的模拟框架在运行时构建一个模拟对象,但我事先不知道生成的类的名称,因为它是一个模拟(所以我无法配置提前,不是我想要的)。

有人在单元测试中成功地使用了这种技术吗?

谢谢。

【问题讨论】:

    标签: java unit-testing dependency-injection junit mocking


    【解决方案1】:

    那篇文章中描述的概念是在后台使用Service LocatorAmbient Context

    由于使用了静态属性和Service Locator,这种模式对于单元测试非常不方便。为了能够运行验证使用此单例的代码的测试,您需要设置一个有效的服务定位器并使用您在使用测试时关心的单例(可能是模拟实例)对其进行配置。

    即使是文章给出的示例也已经存在这些问题,因为“你喜欢单身人士吗?”代码,很难测试:

    if (DialogDisplayer.getDefault().yesOrNo(
        "Do you like singletons?"
    )) {
        System.err.println("OK, thank you!");
    } else {
        System.err.println(
            "Visit http://singletons.apidesign.org to"
            + " change your mind!"
        );
    }
    

    更好的选择是使用构造函数注入来注入该单例(请原谅我的法语,但我不是母语 Java):

    public class AskTheUserController
    {
        private DialogDisplayer dialogDisplayer;
        private MessageDisplayer messageDisplayer;
    
        public AskTheUserController(DialogDisplayer dialogDisplayer,
            MessageDisplayer messageDisplayer)
        {
            this.dialogDisplayer = dialogDisplayer;
            this.messageDisplayer = messageDisplayer;
        }
    
        public void AskTheUser()
        {
            if (this.dialogDisplayer.yesOrNo(
                "Do you like singletons?"
            )) {
                this.messageDisplayer.display("OK, thank you!");
            } else {
                this.messageDisplayer.display(
                    "Visit http://singletons.apidesign.org to"
                    + " change your mind!"
                );
            }
        }
    }
    

    该代码中还有另一个“隐藏”依赖项:System.err.println。它使用MessageDisplayer 接口进行抽象。这段代码有几个明显的优点:

    • 通过注入这两个依赖项,消费者甚至不需要知道这些依赖项是单例的。
    • 代码清楚地传达了它所采用的依赖关系。
    • 可以使用模拟对象轻松测试代码。
    • 测试代码不需要配置服务定位器。

    您的测试可能如下所示:

    @Test
    public void AskTheUser_WhenUserSaysYes_WeThankHim()
    {
        // Arrange
        bool answer = true;
    
        MockMessageDisplayer message = new MockMessageDisplayer();
        MockDialogDisplayer dialog = new MockDialogDisplayer(answer);
    
        AskTheUserController controller =
            new AskTheUserController(dialog, message);
    
        // Act
        controller.AskTheUser();
    
        // Assert
        Assert.AreEqual("OK, thank you!", message.displayedMessage);
    }
    
    @Test
    public void AskTheUser_WhenUserSaysNo_WeLetHimChangeHisMind()
    {
        // Arrange
        bool answer = true;
    
        MockMessageDisplayer message = new MockMessageDisplayer();
        MockDialogDisplayer dialog = new MockDialogDisplayer(answer);
    
        AskTheUserController controller =
            new AskTheUserController(dialog, message);
    
        // Act
        controller.AskTheUser();
    
        // Assert
        Assert.IsTrue(
            message.displayedMessage.contains("change your mind"));
    }
    

    当您使用文章中所示的“可注入单例”模式时,您的测试代码将永远不会像上面的代码那样有意泄露。

    【讨论】:

    • 感谢您的回答。但是,我不是一个纯粹主义者,我不介意某些依赖是隐含的。 + 1 用于详细说明。看来我将定义可覆盖的 getter 属性并在测试期间使用它们来注入模拟。这是目前侵入性最小的方式。
    • 我绝不是一个纯粹主义者。我很实际,但我发现单例是 PITA 的艰难方式,甚至是那篇文章中描述的“可注射单例”模型。我喜欢我的代码和测试简单且可维护,而单例模式通常是实现这一点的方式。
    【解决方案2】:

    单例没有错,单例在任何软件中都是有用且必要的概念。问题是您不应该使用静态字段和方法来实现它们。

    我使用 Guice 来注入我的单例,并且很长一段时间以来我都不必在我的代码库和测试中使用静态。

    这里有几个你可能会觉得有用的链接,它们解释了如何使用 Guice 实现可测试的单例:

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-05-10
      • 2022-01-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-06-21
      • 2015-05-12
      • 1970-01-01
      相关资源
      最近更新 更多