嘲笑单身人士班

Aad*_*mia 52 java oop unit-testing

我最近读到,制作类单例使得无法模拟类的对象,这使得测试其客户端变得困难.我无法立即理解其根本原因.有人可以解释为什么不可能嘲笑单身人士课程?此外,还有更多的问题与制作班级单身人士有关吗?

Pas*_*ent 57

当然,我可以写一些像不使用单身,它们是邪恶的,使用Guice/Spring /除了第一个,这不会回答你的问题,其次,你有时必须处理单身,当使用遗留代码时例.

所以,我们不讨论单身人士的好坏(还有另外一个问题),但让我们看看如何在测试过程中处理它们.首先,让我们看一下单例的常见实现:

public class Singleton {
    private Singleton() { }

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

    public String getFoo() {
        return "bar";
    }
}
Run Code Online (Sandbox Code Playgroud)

这里有两个测试问题:

  1. 构造函数是私有的,所以我们不能扩展它(我们无法在测试中控制实例的创建,但是,这就是单例的重点).

  2. getInstance是静态的,因此很难使用单例在代码中注入伪造而不是单例对象.

对于基于继承和多态的模拟框架,这两点显然都是大问题.如果您拥有对代码的控制权,一个选项是通过添加一个允许调整内部字段的setter来使您的单身"更易于测试",如学习停止担心和爱单身一样(您甚至不需要嘲笑那种情况下的框架).如果不这样做,基于拦截和AOP概念的现代模拟框架可以克服前面提到的问题.

例如,Mocking Static Method Calls显示了如何使用JMockit Expectations模拟Singleton .

另一种选择是使用PowerMock,它是Mockito或JMock的扩展,它允许模拟通常不可模仿的东西,如静态,最终,私有或构造方法.您还可以访问类的内部.

  • @Aadith:这是一个类加载技巧,允许你在不使用`synchronized`的情况下实现单例模式. (9认同)
  • 谢谢Pascal ..这是一个很好的解释..但是你对Singleton的实现让我感到困惑......为什么有内部类来保存单例实例?它是否提供任何特定的优势?到目前为止我看到的所有实现只在类本身中有一个私有静态字段. (6认同)
  • @Aadith这是初始化按需持有人(IODH)成语(http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#dcl),它允许实现懒惰加载单身,零同步开销,如@skaffman所述 (6认同)

bri*_*ndy 14

模拟单身人士的最好方法是根本不使用它们,或者至少不使用传统意义.您可能想要查看的一些实践是:

  • 编程到接口
  • 依赖注入
  • 控制反转

所以,而不是像你这样访问一个:

Singleton.getInstance().doSometing();
Run Code Online (Sandbox Code Playgroud)

...将您的"单身"定义为一个界面,并让其他东西管理它的生命周期并将其注入您需要的地方,例如作为私有实例变量:

@Inject private Singleton mySingleton;
Run Code Online (Sandbox Code Playgroud)

然后,当您单元测试依赖于单例的类/组件/等时,您可以轻松地注入它的模拟版本.

大多数依赖注入容器都允许您将组件标记为"单例",但是由容器来管理它.

使用上述实践可以更轻松地对代码进行单元测试,并让您专注于功能逻辑而不是布线逻辑.它还意味着您的代码真正开始成为真正的面向对象,因为任何静态方法(包括构造函数)的使用都是程序性的.因此,您的组件也开始变得真正可重用.

查看Google Guice作为10的入门者:

http://code.google.com/p/google-guice/

您还可以查看可以执行此类操作的Spring和/或OSGi.那里有很多IOC/DI东西.:)


Pét*_*rök 13

根据定义,Singleton只有一个实例.因此,它的创造受到阶级本身的严格控制.通常它是一个具体的类,而不是一个接口,由于它的私有构造函数,它不是子类.此外,它由客户端(通过调用Singleton.getInstance()或等效)主动发现,因此您不能轻易使用例如依赖注入将模拟实例替换为"真实"实例:

class Singleton {
    private static final myInstance = new Singleton();
    public static Singleton getInstance () { return myInstance; }
    private Singleton() { ... }
    // public methods
}

class Client {
    public doSomething() {
        Singleton singleton = Singleton.getInstance();
        // use the singleton
    }
}
Run Code Online (Sandbox Code Playgroud)

对于模拟,理想情况下,您需要一个可以自由子类化的接口,并通过依赖注入将其具体实现提供给其客户端.

您可以放松Singleton实现以使其可测试

  • 提供可以由模拟子类以及"真实"子类实现的接口
  • 添加一个setInstance方法以允许在单元测试中替换实例

例:

interface Singleton {
    private static final myInstance;
    public static Singleton getInstance() { return myInstance; }
    public static void setInstance(Singleton newInstance) { myInstance = newInstance; }
    // public method declarations
}

// Used in production
class RealSingleton implements Singleton {
    // public methods
}

// Used in unit tests
class FakeSingleton implements Singleton {
    // public methods
}

class ClientTest {
    private Singleton testSingleton = new FakeSingleton();
    @Test
    public void test() {
        Singleton.setSingleton(testSingleton);
        client.doSomething();
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

如您所见,您只能通过破坏Singleton的"清洁度"来使您的Singleton使用代码单元可测试.最后,如果你能避免它,最好不要使用它.

更新:以下是Michael Feathers 对"有效使用遗留代码"的强制性参考.

  • 谢谢你的回答.快问.如何在接口`Singleton`中,在`setInstance()`方法中我们可以将最终变量`myInstance`设置为什么? (2认同)

Boz*_*zho 8

它在很大程度上取决于单例实现.但它主要是因为它有一个私有构造函数,因此你无法扩展它.但是你有以下选择

  • 制作界面 - SingletonInterface
  • 让你的单例类实现该接口
  • 让我们Singleton.getInstance()回来SingletonInterface
  • SingletonInterface在测试中提供模拟实现
  • 使用反射在private static字段中设置它Singleton.

但你最好避免单身人士(代表一个全球国家).本讲座从可测试性的角度解释了一些重要的设计概念.


Mar*_*ers 5

并不是说单例模式本身就是纯粹的邪恶,而是即使在不适当的情况下也被大量过度使用。许多开发人员认为“哦,我可能只需要其中一个,所以让我们将其设为单例”。事实上,您应该考虑“我可能只需要其中一个,所以让我们在程序开始时构建一个,并在需要的地方传递引用。”

单身和测试的第一个问题与其说是因为单身,不如说是因为懒惰。由于获取单例的便利性,对单例对象的依赖通常直接嵌入到方法中,这使得将单例更改为具有相同接口但具有不同实现的另一个对象(例如,模拟对象)变得非常困难)。

代替:

void foo() {
   Bar bar = Bar.getInstance();
   // etc...
}
Run Code Online (Sandbox Code Playgroud)

更喜欢:

void foo(IBar bar) {
   // etc...
}
Run Code Online (Sandbox Code Playgroud)

现在您可以foo使用bar您可以控制的模拟对象测试功能。您已删除依赖项,以便foo无需测试即可进行测试bar

单例和测试的另一个问题是在测试单例本身时。单例(按设计)很难重建,因此例如您只能测试单例构造函数一次。单个实例也可能Bar在测试之间保留状态,导致成功或失败取决于测试运行的顺序。