【问题标题】:Why is a singleton class hard to test?为什么单例类很难测试?
【发布时间】:2014-08-12 20:08:43
【问题描述】:

Effective Java 第 3 项(使用私有构造函数或枚举类型强制执行单例属性)指出:

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

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

我见过各种“解释”,或多或少是对上述引文的改写。有人可以进一步解释一下吗,最好用代码示例?

【问题讨论】:

  • 引用不是关于测试单例,而是关于测试使用单例的代码。
  • 现在你测试客户端了吗?在 unit 测试中,您需要模拟依赖项并仅测试 CUT。当客户端使用对Singleton.INSTANCE 的硬编码引用时,您如何做到这一点?
  • 这实际上对statics 来说是正确的(也就是说,任何以静态方式访问的东西)
  • 由于现在单例被注入并且不是以静态方式访问的,所以不要在这句话上过分努力。
  • 感谢@chris-martin 指出这一点。

标签: java singleton


【解决方案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());
}

【讨论】:

  • 但似乎MySingletonMyNonSingleton 之间的主要区别在于MyNonSingleton 实现了一个用作类型的接口,而不是MyNonSingleton 不是单例。如果MySingleton 仍然是单例,但实现了ISomeInterface,您不能以相同的方式对其进行测试(使用实现相同接口的MockSingleton)吗?
  • @coder123 但是您将如何更改单元测试中使用的单例?
  • @mclaassen 我为你写了一点Java :) 为一般性讨论做出贡献。 powermock 可以让你模拟静态方法。因此,当您选择使用 Josh 关于可测试性的观点时,它的问题就不大了。
  • @mclaassen Gotcha。谢谢!
  • @ThomasJungblut 谢谢。我已经有一段时间没有使用 Java 了。
【解决方案2】:

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

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

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

【讨论】:

    【解决方案3】:

    不可能用模拟实现代替单例

    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 精彩的帖子。顺便说一句,单元测试状态危险不是一个很好的例子(它不是一个单元测试),但明白这一点更重要。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-01-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-04
      • 2011-12-13
      • 1970-01-01
      相关资源
      最近更新 更多