java 为什么单例类很难测试?

oyxsuwqo  于 2023-06-04  发布在  Java
关注(0)|答案(4)|浏览(472)

Effective JavaItem 3(使用私有构造函数或枚举类型强制执行singleton属性)注意:

使类成为单例会使测试它的客户端变得困难,因为不可能用模拟实现来代替单例,除非它实现了一个作为其类型的接口。
出于测试目的,为什么示例化一个单例示例并测试其API还不够?这不就是客户要消费的东西吗这句话似乎暗示测试单例将涉及“模拟实现”,但为什么有必要呢?
我看到过各种各样的“解释”,或多或少都是对上面引用的话的改写。有人能进一步解释一下吗,最好是用代码示例?

b09cbbtk

b09cbbtk1#

如果你的单例是在数据库上执行操作或向文件中写入数据呢?您不希望在单元测试中发生这种情况。您可能希望模拟对象以在内存中执行某些操作,这样您就可以在没有永久副作用的情况下验证它们。单元测试应该是自包含的,不应该创建到数据库的连接,也不应该使用外部系统执行其他操作,这些操作可能会失败,然后导致单元测试因不相关的原因而失败。
pseudo-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());
}
yhuiod9q

yhuiod9q2#

我个人认为这种说法完全是错误的,因为它假设单例对于单元测试是不可替换的(可模拟的)。恰恰相反。例如,在Spring的依赖注入中,singleton实际上是DI组件的默认模型。单例和依赖注入并不是相互排斥的,上面的陈述试图暗示这一点。
我同意任何不能被模仿的东西都会使应用程序更难测试,但是没有理由认为单例比应用程序中的任何其他对象都更难模仿
可能的问题是,单例是一个全局示例,当它可以处于太多不同的状态时,单元测试可能会因为单例的状态变化而显示不可预测的结果。但是有一些简单的解决方案-模拟你的单例,让你的模拟有更少的状态。或者以这样一种方式编写测试,即在依赖于它的每个单元测试之前重新创建(或重新初始化)单例。或者,最好的解决方案是,针对单例的所有可能状态测试应用程序。最终,如果现实需要多个状态,例如数据库连接(断开/连接/连接/错误/...),那么无论是否使用单例,您都必须处理它。

mxg2im7a

mxg2im7a3#

无法用mock实现替代singleton

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

public class Main {
    @Test
    void test(){
        SpellChecker s = Mockito.mock(SpellChecker.class); //IMPOSSIBLE
        when(s.check(any())).thenReturn(false);
        Client c = new Client(s);
        assertThat(c.check("abc")).isEqualTo(false);
    }
}

class SpellChecker{
    private static final SpellChecker INSTANCE = new SpellChecker();
    private SpellChecker(){throw new AssertionError();}
    public boolean check(String word){return true;}
    public static SpellChecker getInstance(){return INSTANCE;}
}

class Client{
    private SpellChecker s;
    Client(SpellChecker s){this.s=s;}
    boolean check(String str){return s.check(str);}
}

除非它实现了一个作为其类型的接口。

public class Main {
    @Test
    void test(){
        SpellCheckerI s = Mockito.mock(SpellCheckerI.class); //POSSIBLE
        when(s.check(any())).thenReturn(false);
        Client c = new Client(s);
        assertThat(c.check("abc")).isEqualTo(false);
    }
}

interface SpellCheckerI{boolean check(String word);}

class SpellChecker implements SpellCheckerI{
    private static final SpellChecker INSTANCE = new SpellChecker();
    private SpellChecker(){throw new AssertionError();}
    @Override public boolean check(String word){return true;}
    public static SpellChecker getInstance(){return INSTANCE;}
}

class Client{
    private SpellCheckerI s;
    Client(SpellCheckerI s){this.s=s;}
    boolean check(String str){return s.check(str);}
}

P.S.你可能也想看看this的精彩文章。顺便说一句,单元测试状态danger不是一个很好的例子(它不是一个单元测试),但是抓住重点更重要。

gfttwv5a

gfttwv5a4#

如果你愿意的话,你可以用一个模拟的实现来代替一个单例

import static org.mockito.Mockito.mockConstruction;

try (MockedConstruction<SpellChecker> mocked = mockConstruction(SpellChecker.class,
            (mock, context) -> {
               // other mock and when-thenReturn constructs
    
            })) {
    
        // assertions here
    }

相关问题