我最近读过,制作类单例使得无法模拟类的对象,这使得测试其客户端变得困难。我无法立即理解其根本原因。有人可以解释为什么不可能嘲笑单身人士课程?另外,还有更多问题与制作班级单身人士相关吗?
答案 0 :(得分:56)
当然,我可以写一些像不要使用单身,它们是邪恶的,使用Guice / Spring /无论,但首先,这不会回答你的问题,其次,你有时< strong>必须处理单例,例如使用遗留代码时。
所以,我们不讨论单身人士的好坏(这里还有另一个question),但让我们看看如何在测试过程中处理它们。首先,让我们看一下单例的常见实现:
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";
}
}
这里有两个测试问题:
构造函数是私有的,所以我们不能扩展它(我们无法在测试中控制实例的创建,但是,这就是单例的重点)。
getInstance
是静态的,因此很难使用单例在代码中注入伪造而不是单例对象。
对于基于继承和多态的模拟框架,这两点显然都是大问题。如果您可以控制代码,一个选项是通过添加一个允许调整内部字段的setter来使您的单例“更易于测试”,如Learn to Stop Worrying and Love the Singleton中所述(您甚至不需要一个模拟框架)案件)。如果不这样做,基于拦截和AOP概念的现代模拟框架可以克服前面提到的问题。
例如,Mocking Static Method Calls显示了如何使用JMockit Expectations模拟单身人士。
另一种选择是使用PowerMock,Mockito或JMock的扩展,它允许模拟通常不可模仿的东西,如静态,最终,私有或构造方法。您还可以访问类的内部。
答案 1 :(得分:14)
模拟单身人士的最佳方法是根本不使用它们,或者至少不使用传统意义。您可能想要查看的一些实践是:
所以,不要像你这样访问一个人:
Singleton.getInstance().doSometing();
...将您的“单身人士”定义为一个界面,并让其他东西管理它的生命周期并将其注入您需要的地方,例如作为私有实例变量:
@Inject private Singleton mySingleton;
然后,当您单元测试依赖于单例的类/组件/等时,您可以轻松地注入它的模拟版本。
大多数依赖注入容器都会让你将组件标记为“singleton”,但是由容器来管理它。
使用上述实践可以更轻松地对代码进行单元测试,并让您专注于功能逻辑而不是布线逻辑。它还意味着您的代码真正开始成为真正的面向对象,因为任何静态方法(包括构造函数)的使用都是程序性的。因此,您的组件也开始变得真正可重用。
将Google Guice视为10的首发
http://code.google.com/p/google-guice/
你也可以看看可以做这种事情的Spring和/或OSGi。那里有很多IOC / DI东西。 :)
答案 2 :(得分:13)
根据定义,Singleton只有一个实例。因此,它的创造受到阶级本身的严格控制。通常它是一个具体的类,而不是一个接口,由于它的私有构造函数,它不是子类。此外,它的客户(通过调用Singleton.getInstance()
或等效的)主动发现它,因此您不能轻易使用例如Dependency Injection用模拟实例替换它的“真实”实例:
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
}
}
对于模拟,理想情况下,您需要一个可以自由子类化的接口,并通过依赖注入将其具体实现提供给其客户端。
您可以通过
放松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();
// ...
}
}
如您所见,您只能通过破坏Singleton的“清洁度”来使您的Singleton使用代码单元可测试。最后,如果你能避免它,最好不要使用它。
更新:以下是Michael Feathers对Working Effectively With Legacy Code的强制性引用。
答案 3 :(得分:9)
这在很大程度上取决于单例实现。但它主要是因为它有一个私有构造函数,因此你无法扩展它。但是您有以下选项
SingletonInterface
Singleton.getInstance()
返回SingletonInterface
SingletonInterface
的模拟实现private static
上的Singleton
字段中进行设置。但你最好避免单身人士(代表全球状态)。 This lecture从可测试性的角度解释了一些重要的设计概念。
答案 4 :(得分:3)
并不是说Singleton模式本身就是纯粹的邪恶,但即使在它不合适的情况下,它也会大量过度使用。许多开发人员认为“哦,我可能只需要其中一个,所以让它成为一个单身人士”。事实上你应该想“我可能只需要其中一个,所以让我们在我的程序开始时构造一个并在需要的地方传递引用。”
单身和测试的第一个问题不是因为单身而是因为懒惰。由于获取单例的便利性,对单例对象的依赖通常直接嵌入到方法中,因此很难将单例更改为具有相同接口但具有不同实现的另一个对象(例如,模拟对象) )。
而不是:
void foo() {
Bar bar = Bar.getInstance();
// etc...
}
喜欢:
void foo(IBar bar) {
// etc...
}
现在,您可以使用可以控制的模拟foo
对象来测试函数bar
。您已删除相关性,以便在不测试foo
的情况下测试bar
。
单例和测试的另一个问题是测试单例本身。单例(按设计)很难重构,因此例如,您只能测试一次单例构造函数。 Bar
的单个实例也可能在测试之间保持状态,从而导致成功或失败,具体取决于运行测试的顺序。
答案 5 :(得分:1)
有一种模拟Singleton的方法。使用powermock模拟静态方法,并使用Whitebox调用构造函数YourClass mockHelper = Whitebox
.invokeConstructor(YourClass.class);
Whitebox.setInternalState(mockHelper, "yourdata",mockedData);
PowerMockito.mockStatic(YourClass.class);
Mockito.when(YourClass.getInstance()).thenReturn(mockHelper);
正在发生的事情是Singleton字节代码在运行时发生了变化。
享受