有效Java 第3项(使用私有构造函数或枚举类型强制执行单例属性)注意到:
使类成为单例可能会使测试其客户端变得困难,因为除非它实现了作为其类型的接口,否则不可能将模拟实现替换为单例。
出于测试目的,为什么仅实例化单个单例实例并测试其API是不够的?不是客户会消费的吗?引用似乎意味着测试单例将涉及"模拟实现,"但为什么这有必要?
我已经看过各种"解释"这或多或少是对上述引用的重新修改。有人可以进一步解释这一点,最好是用代码示例吗?
答案 0 :(得分:10)
如果您的单身人士正在对数据库执行操作或将数据写入文件,该怎么办?你不希望在单元测试中发生这种情况。您可能希望模拟对象以在内存中执行某些操作,以便您可以验证它们而不会产生永久的副作用。单元测试应该是自包含的,不应该创建与数据库的连接,也不应该对可能发生故障的外部系统执行其他操作,然后导致单元测试因不相关的原因而失败。
伪java的示例(我是C#dev):
public class MySingleton {
private static final MySingleton instance = new MySingleton();
private MySingleton() { }
public int doSomething() {
//create connection to database, write to a file, etc..
return something;
}
public static MySingleton getInstance() {
return instance;
}
}
public class OtherClass {
public int myMethod() {
//do some stuff
int result = MySingleton.getInstance().doSomething();
//do some other suff
return something;
}
}
为了测试myMethod
,我们必须进行实际的数据库调用,文件操作等
@Test
public void testMyMethod() {
OtherClass obj = new OtherClass();
//if this fails it might be because of some external code called by
//MySingleton.doSomething(), not necessarily the logic inside MyMethod()
Asserts.assertEqual(1, obj.myMethod());
}
如果MySingleton
更像是:
public class MyNonSingleton implements ISomeInterface {
public MyNonSingleton() {}
@Override
public int doSomething() {
//create connection to database, write to a file, etc..
return something;
}
}
然后你可以将它作为依赖注入到MyOtherClass中,如下所示:
public class OtherClass {
private ISomeInterface obj;
public OtherClass(ISomeInterface obj) {
this.obj = obj;
}
public int myMethod() {
//do some stuff
int result = obj.doSomething();
//do some other stuff
return something;
}
}
然后你可以这样测试:
@Test
public void TestMyMethod() {
OtherClass obj = new OtherClass(new MockNonSingleton());
//now our mock object can fake the database, filesystem etc. calls to isolate the testing to just the logic in myMethod()
Asserts.assertEqual(1, obj.myMethod());
}
答案 1 :(得分:0)
我个人认为此声明完全错误,因为它假定单例对于单元测试是不可替代的(可模拟的)。反之。例如,在Spring的依赖项注入中,单例实际上是DI组件的默认模型。单例和依赖注入不是互斥的,上面的陈述试图以某种方式暗示。
我同意,任何无法模拟的东西都会使应用程序更加难以测试,但是没有理由认为单例比您的应用程序中的任何其他对象都更容易模拟。
可能的问题是,单例是一个全局实例,并且当它可以处于太多不同状态时,由于单例状态的更改,单元测试可能会显示不可预测的结果。但是,对此有简单的解决方案-模拟您的单身人士,并使您的模拟人拥有更少的状态。或以这种方式编写测试,即在依赖它的每个单元测试之前重新创建(或重新初始化)单例。或者,最好的解决方案是测试应用程序的单例所有可能状态。最终,如果现实需要多个状态,例如数据库连接(断开/正在连接/已连接/错误/ ...),那么无论您是否使用单例,您都必须处理它。