设计可测试性的构造函数

时间:2009-09-23 21:16:39

标签: java unit-testing refactoring constructor

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

原始构造函数:

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”。

这对我来说很难:我无法弄清楚如何在其他地方简单地推动这些“新”调用来打破这一系列事件。我错过了什么?

6 个答案:

答案 0 :(得分:7)

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

introduction to Spring(pdf)全面概述了Spring。第一章或第二章应该足以解释这些概念。

另见Martin Fowler的Inversion of Control Containers and the Dependency Injection pattern

答案 1 :(得分:2)

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

  1. 永远不会实例化或自我获取依赖
  2. 始终编程到接口
  3. 你从规则#1开始。您将Info类更改为不再创建其依赖项。 “依赖”我指的是其他类,从属性文件或其他任何东西加载的配置数据等。当你依赖于实例化的东西时,你将你的类绑定到它并使其更难以测试,重用和维护。因此,即使通过工厂或单例创建依赖关系,也不要让您的类创建它。请其他人调用create()getInstance()或其他任何内容并将其传入。

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

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

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

答案 2 :(得分:1)

即使像Spring,Guice,PicoContainer等依赖注入框架也需要某种类型的boostrap,所以你总是需要构建一些东西。

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

答案 3 :(得分:1)

您的构造函数不正确,问题不在于执行代码的时间/位置,而是关于其他人提到的内容:依赖注入。您需要创建模拟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
    }
}

答案 4 :(得分:0)

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

答案 5 :(得分:0)

你让其他人对Info类及其依赖关系有太多了解。 依赖注入框架将使用提供程序类。使用泛型类型可以创建Info of Info对象:

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

如果您的其他人采取了提供商&lt; Info&gt;在它的构造函数中,您的方法看起来像:

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

这已从您的代码中删除了具体类的构造。下一步是将Info设置为一个界面,以便更容易为特定的单元测试用例创建模拟。

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

我建议阅读Misko Heverys blog。所有演示文稿都很有用,并且一旦您理解了规则是一件好事就打印出将可测试代码编写为小规则手册的指南。