【问题标题】:Designing Constructors for Testability为可测试性设计构造函数
【发布时间】:2009-09-23 21:16:38
【问题描述】:

我正在处理一些现有的代码,试图添加它并增加它的单元测试。但是在让代码可测试时遇到了一些问题。

原始构造函数:

public Info() throws Exception
{
  _ServiceProperties = new ServiceProperties();
  _SshProperties = new SshProperties();
}

我知道这很糟糕,而且显然无法测试。在 junit 环境中,此类将无法每次创建,因为它无法找到构建自身所需的属性。现在,我知道通过移动以“new”开头的任何内容作为参数的简单更改,这个类将更具可测试性。

所以我最终得到:

新构造函数:

public Info(ServiceProperties srvProps, SshProperties sshProps) throws Exception
{
  _ServiceProperties = srvProps;
  _SshProperties = sshProps;
}

这让我可以正确地对这个 Info 类进行单元测试。但问题是,现在所有的工作都被推到了其他类:

其他类的方法:

public void useInfo() throws Exception
{
  ServiceProperties srvProps = new ServiceProperties();
  SshProperties sshProps = new SshProperties();
  Info info = new Info(srvProprs, sshProprs);
  doStuffWithInfo(info);
}

现在这个方法是不可测试的。我所能做的就是将这些 Property 对象的构造推到发生的地方,而在其他地方,一些代码实际上会被卡住,实际上不得不调用“new”。

这对我来说是个难题:我不知道如何打破将这些“新”调用简单地推送到其他地方的事件链。我错过了什么?

【问题讨论】:

    标签: java unit-testing refactoring constructor


    【解决方案1】:

    看看使用依赖注入框架,例如Spring。这种控制反转的应用意味着您的每种类型都可以避免您所看到的陷阱,让配置将组件“连接”在一起。

    introduction to Spring (pdf) 提供了 Spring 的全面概述。前一两章应该足以解释这些概念。

    另请参阅 Martin Fowler 的 Inversion of Control Containers and the Dependency Injection pattern

    【讨论】:

    【解决方案2】:

    你的想法是对的。也许这会对你有所帮助。我建议您对所有重要类遵循两条规则,其中“重要”意味着如果您不遵循这些步骤,则测试、重用或维护该类将更加困难。规则如下:

    1. 从不实例化或自行获取依赖项
    2. 始终按接口编程

    您从规则 #1 开始。您更改了 Info 类以不再创建其依赖项。 “依赖关系”是指其他类、从属性文件加载的配置数据或其他任何东西等。当您依赖某物的实例化方式时,您将您的类与它绑定在一起,并使其更难以测试、重用和维护。因此,即使依赖项是通过工厂或单例创建的,也不要让您的类创建它。有别的东西打电话给create()getInstance() 或其他什么,然后把它传进去。

    所以你选择了“别的东西”作为使用你的类的类,并意识到它有一股难闻的气味。补救方法是让应用程序的入口点实例化所有依赖项。在传统的 Java 应用程序中,这是您的 main() 方法。如果您考虑一下,实例化类并将它们相互连接,或将它们“连接”在一起,是一种特殊的逻辑:“应用程序组装”逻辑。是在整个代码中传播此逻辑更好,还是将其收集在一个地方以便更轻松地维护它?答案是,将它收集在一个地方会更好 - 不仅是为了维护,而且这样做可以将所有重要类别变成更有用和更灵活的组件。

    在您的main() 或等效的main() 中,您应该创建您需要的所有对象,将它们传递给彼此的设置器和构造器以将它们“连接”在一起。然后,您的单元测试将以不同的方式连接它们,传入模拟对象或类似的东西。做这一切的行为被称为“依赖注入”。按照我说的做之后,你可能会有一个大丑陋的main() 方法。这就是依赖注入工具可以帮助您并实际上使您的代码更加灵活的地方。正如其他人所建议的那样,当您达到这一点时,我建议的工具是 Spring。

    不太重要的规则 #2 - 始终编程到接口,仍然非常重要,因为它消除了对实现的所有依赖,使重用变得更加容易,更不用说利用其他工具,如模拟对象框架、ORM 框架等。嗯。

    【讨论】:

      【解决方案3】:

      即使是 Spring、Guice、PicoContainer 等依赖注入框架也需要某种 boostrap,因此您总是需要构建一些东西。

      我建议您使用返回您的类的配置实例的提供者/工厂。这将允许您退出“创建”层次结构。

      【讨论】:

        【解决方案4】:

        您的构造函数没有错误,问题不在于何时/何地执行代码,而在于其他人提到的:依赖注入。您需要创建模拟 SshProperties 对象来实例化您的对象。最简单的方法(假设该类未标记为 final)是扩展该类:

        public class MockSshProperties extends SshProperties {
            // implemented methods
        }
        

        你可以使用像Mockito这样的模拟框架:

        public class Info {
        
            private final sshProps;
            private final serviceProps;
        
            public Info() {
                this(new SshProperties(), new ServiceProperties());
            }
        
            public Info(SshProperties arg1, ServiceProperties arg2) {
                this.sshProps = arg1;
                this.serviceProps = arg2
            }
        }
        
        public class InfoTester
        {
            private final static SshProperties sshProps = mock(SshProperties.class);
            private final static ServiceProperties serviceProps = mock(ServiceProperties.class);
            static {
                when(sshProps.someGetMethod("something")).thenReturn("value");
            }
        
            public static void main(String[] args) {
                Info info = new Info(sshProps, serviceProps);
                //do stuff
            }
        }
        

        【讨论】:

          【解决方案5】:

          最简单的答案是 Spring。然而,另一个答案是将您的配置内容放入 JNDI。 在某些方面,Spring 是一个更好的答案,特别是如果您没有任何因环境而变化的东西。

          【讨论】:

            【解决方案6】:

            您让其他类对 Info 类及其依赖项了解太多。 依赖注入框架会使用提供者类。使用泛型类型可以创建 Info 对象的提供者:

            interface Provider<T> {
              T get();
            }
            

            如果您的其他类在其构造函数中使用 Provider,您的方法将如下所示:

            public void useInfo() throws Exception
            {
              Info info = infoProvider.get();
              doStuffWithInfo(info);
            }
            

            这已经从您的代码中删除了具体类的构造。下一步是将 Info 变成一个接口,以便更轻松地为特定的单元测试用例创建模拟。

            是的,这将推动所有对象构造代码越来越高。它将导致一些仅描述如何将事物连接在一起的模块。这种代码的“规则”是它应该没有条件语句和循环。

            我推荐阅读Misko Heverys blog。所有的演示文稿都很有用,一旦您了解了规则,将编写可测试代码的指南打印成小规则书是一件好事。

            【讨论】:

              猜你喜欢
              • 2012-12-29
              • 2013-09-28
              • 1970-01-01
              • 1970-01-01
              • 2012-01-28
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多