为什么单例类很难测试?
有效的Java项目 3(使用私有构造函数或枚举类型强制实施单例属性)指出:
使类成为单例可能会使测试其客户端变得困难,因为除非它实现作为其类型的接口,否则不可能用模拟实现代替单例。
出于测试目的,为什么实例化单一实例并测试其 API 还不够?这难道不是客户将要消费的吗?这句话似乎暗示测试单例将涉及“模拟实现”,但为什么这是必要的呢?
我看到过各种“解释”,这些解释或多或少是对上面引用的改写。有人可以进一步解释这一点,最好是用代码示例吗?
有效的Java项目 3(使用私有构造函数或枚举类型强制实施单例属性)指出:
使类成为单例可能会使测试其客户端变得困难,因为除非它实现作为其类型的接口,否则不可能用模拟实现代替单例。
出于测试目的,为什么实例化单一实例并测试其 API 还不够?这难道不是客户将要消费的吗?这句话似乎暗示测试单例将涉及“模拟实现”,但为什么这是必要的呢?
我看到过各种“解释”,这些解释或多或少是对上面引用的改写。有人可以进一步解释这一点,最好是用代码示例吗?
如果您的单例正在对数据库执行操作或将数据写入文件,该怎么办?您不希望在单元测试中发生这种情况。您可能希望模拟对象以在内存中执行某些操作,以便您可以验证它们而不会产生永久性的副作用。单元测试应是自包含的,不应创建与数据库的连接,也不应使用外部系统执行其他操作,这些操作可能会失败,然后导致单元测试因不相关的原因而失败。
伪 java 示例(我是 C# 开发人员):
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());
}
我个人认为这种说法是完全错误的,因为它假设单例对于单元测试是不可替换(可模拟的)的。恰恰相反。例如,在Spring的依赖注入中,单例实际上是DI组件的默认模型。单例和依赖注入并不相互排斥,上面的语句试图以某种方式暗示这一点。
我同意任何不能被模拟的东西都会使应用程序更难以测试,但是没有理由假设单例比应用程序中的任何其他对象都更不可模拟。
可能的问题是,单例是一个全局实例,当它处于太多不同的状态时,单元测试可能会由于单例状态的变化而显示不可预测的结果。但是有简单的解决方案 - 模拟你的单例,让你的模拟具有更少的状态。或者以这种方式编写测试,在依赖于它的每个单元测试之前重新创建(或重新初始化)单例。或者,最佳解决方案是测试应用程序是否存在单例的所有可能状态。最终,如果现实需要多种状态,例如,数据库连接(断开连接/连接/连接/错误/...),那么无论你是否使用单例,你都必须处理它。