为什么单例类很难测试?

2022-09-03 06:06:46

有效的Java项目 3(使用私有构造函数或枚举类型强制实施单例属性)指出:

使类成为单例可能会使测试其客户端变得困难,因为除非它实现作为其类型的接口,否则不可能用模拟实现代替单例。

出于测试目的,为什么实例化单一实例并测试其 API 还不够?这难道不是客户将要消费的吗?这句话似乎暗示测试单例将涉及“模拟实现”,但为什么这是必要的呢?

我看到过各种“解释”,这些解释或多或少是对上面引用的改写。有人可以进一步解释这一点,最好是用代码示例吗?


答案 1

如果您的单例正在对数据库执行操作或将数据写入文件,该怎么办?您不希望在单元测试中发生这种情况。您可能希望模拟对象以在内存中执行某些操作,以便您可以验证它们而不会产生永久性的副作用。单元测试应是自包含的,不应创建与数据库的连接,也不应使用外部系统执行其他操作,这些操作可能会失败,然后导致单元测试因不相关的原因而失败。

伪 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());
}

答案 2

我个人认为这种说法是完全错误的,因为它假设单例对于单元测试是不可替换(可模拟的)的。恰恰相反。例如,在Spring的依赖注入中,单例实际上是DI组件的默认模型。单例和依赖注入并不相互排斥,上面的语句试图以某种方式暗示这一点。

我同意任何不能被模拟的东西都会使应用程序更难以测试,但是没有理由假设单例比应用程序中的任何其他对象都更不可模拟

可能的问题是,单例是一个全局实例,当它处于太多不同的状态时,单元测试可能会由于单例状态的变化而显示不可预测的结果。但是有简单的解决方案 - 模拟你的单例,让你的模拟具有更少的状态。或者以这种方式编写测试,在依赖于它的每个单元测试之前重新创建(或重新初始化)单例。或者,最佳解决方案是测试应用程序是否存在单例的所有可能状态。最终,如果现实需要多种状态,例如,数据库连接(断开连接/连接/连接/错误/...),那么无论你是否使用单例,你都必须处理它。


推荐