单身和unit testing

Effective Java在unit testing单例上有以下声明

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

任何人都可以解释为什么会这样吗?

您可以使用reflection来重置单个对象,以防止测试相互影响。

@Before public void resetSingleton() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Field instance = MySingleton.class.getDeclaredField("instance"); instance.setAccessible(true); instance.set(null, null); } 

参考: unit testing – 单例

模拟需要接口,因为您正在做的是用模仿测试所需内容的模板取代真实的基础行为。 由于客户端只处理接口引用类型,因此不需要知道实现是什么。

您不能在没有接口的情况下模拟具体类,因为如果没有测试客户端知道它,您就无法替换该行为。 在这种情况下,这是一个全新的课程。

所有类都是如此,Singleton与否。

我认为这实际上取决于单例访问模式的实现

例如

 MySingleton.getInstance() 

可能非常难以测试

 MySingletonFactory mySingletonFactory = ... mySingletonFactory.getInstance() //this returns a MySingleton instance or even a subclass 

不提供有关使用单例的事实的任何信息。 所以你可以自由更换你的工厂。

注意 :单例是通过在应用程序中只是该类的一个实例来定义的,但是获取或存储它的方式不必通过静态方式。

这很简单哦。

在unit testing中,您希望隔离您的SUT(您正在测试的类)。 你不想测试一堆类 ,因为这会破坏单元 测试的目的。

但并非所有class级都能自己做所有事情 ,对吧? 大多数类使用其他类来完成他们的工作,并且它们在其他类之间进行调解,并添加一些自己的类,以获得最终结果。

关键是 – 你不关心你的SUT课程如何依赖于工作。 您关心SUT如何与这些类一起使用 。 这就是你存根或模拟你的SUT需要的类的原因。 您可以使用这些模拟,因为您可以将它们作为SUT的构造函数参数传递。

对于单身人士 – 糟糕的是getInstance()方法是全局可访问的。 这意味着您通常类中调用它,而不是依赖于稍后可以模拟的接口。 这就是为什么当你想测试你的SUT时不可能更换它。

解决方案不是使用偷偷摸摸的public static MySingleton getInstance()方法,而是依赖于您的类需要使用的接口 。 这样做,你可以随时传递测试双打

问题不在于测试单身人士自己; 这本书说,如果你试图测试的课程取决于单身,那么你可能会有问题。

除非,即,你(1)使单例实现一个接口,(2)使用该接口将单例注入你的类。

例如,单例通常直接实例化为:

 public class MyClass { private MySingleton __s = MySingleton.getInstance() ; ... } 

MyClass现在可能很难自动测试。 例如,正如@BorisPavlović在他的回答中所指出的,如果单例的行为是基于系统时间的,那么你的测试现在也取决于系统时间,你可能无法测试那些依赖于系统时间的情况。一周中的天。

但是,如果你的单例“实现了一个作为其类型的接口”,那么你仍然可以使用该接口的单例实现 ,只要你传入它:

 public class SomeSingleton implements SomeInterface { ... } public class MyClass { private SomeInterface __s ; public MyClass( SomeInterface s ) { __s = s ; } ... } ... MyClass m = new MyClass( SomeSingleton.getInstance() ) ; 

从测试MyClass的角度来看,你现在不关心SomeSingleton是否是单例:你也可以传入你想要的任何其他实现,包括单例实现,但很可能你会使用你控制的某种类型的模拟从你的测试。

顺便说一下,这不是这样做的方法:

 public class MyClass { private SomeInterface __s = SomeSingleton.getInstance() ; public MyClass() { } ... } 

这在运行时仍然有效,但是对于测试,你现在再次依赖于SomeSingleton

在没有任何外部控制的情况下创建单例对象。 在同一本书的其他章节中,Bloch建议使用enum作为默认的Singleton实现。 我们来看一个例子吧

 public enum Day { MON(2), TUE(3), WED(4), THU(5), FRI(6), SAT(7), SUN(1); private final int index; private Day(int index) { this.index = index; } public boolean isToday() { return index == new GregorianCalendar().get(Calendar.DAY_OF_WEEK); } } 

假设我们的代码只能在周末执行:

 public void leisure() { if (Day.SAT.isToday() || Day.SUN.isToday()) { haveSomeFun(); return; } doSomeWork(); } 

测试休闲方法将非常困难。 它的执行将取决于它执行的那一天。 如果它在工作日执行,则会调用haveSomeFun()并在周末使用haveSomeFun()

对于这种情况,我们需要使用一些重型工具,如PowerMock来拦截GregorianCalendar构造函数,返回一个模拟,它将返回一个对应于工作日或周末的索引,在两个测试用例中测试leisure方法的两个执行路径。

 it's impossible to substitute a mock implementation for a singleton 

这不是真的。 你可以子类化你的单例和setter注入一个mock。 或者,您可以使用PowerMock来模拟静态方法。 然而,模仿单身人士的需要可能是设计不佳的症状。

真正的问题是单身人士在被滥用时变成依赖性磁铁 。 由于它们随处可访问,因此将所需的函数放入其中可能看起来更方便,而不是委托给适当的类,尤其是对于刚接触OOP的程序员。

可测试性问题现在你有一堆单身人士可以被你正在测试的对象访问。 即使对象可能只使用单例中的一小部分方法,您仍然需要模拟每个Singleton并找出依赖于哪些方法。 具有静态(Monostate模式)的单例更糟糕,因为您必须弄清楚对象之间的哪些交互受Singleton状态的影响。

仔细使用,单身人士和可测试性可以一起发生。 例如,在没有DI框架的情况下,您可以使用Singletons作为您的Factories和ServiceLocators,您可以设置注入以为您的端到端测试创建虚假服务层。

据我所知,实现Singleton的类不能扩展(超类构造函数总是隐式调用,而Singleton中的构造函数是私有的)。 如果你想模拟一个类,你必须扩展该类。 正如你在这种情况下看到的那样,这是不可能的。

单例(以及静态方法)的问题在于它很难用模拟实现替换实际代码。

例如,请考虑以下代码

 public class TestMe() { public String foo(String data) { boolean isFeatureFlag = MySingletonConfig.getInstance().getFeatureFlag(); if (isFeatureFlag) // do somethine with data else // do something else with the data return result; } } 

为foo方法编写unit testing并validation是否执行了正确的行为并不容易。 这是因为您无法轻松更改getFeatureFlag的返回值。

静态方法存在同样的问题 – 用模拟行为替换实际的目标类方法并不容易。

当然,有一些解决方法,如powermock ,或方法的dependency injection,或测试中的reflection。 但最好不要首先使用单身人士

有可能,参见示例

 import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.lang.reflect.Field; import org.junit.After; import org.junit.Before; import org.junit.Test; public class DriverSnapshotHandlerTest { private static final String MOCKED_URL = "MockedURL"; private FormatterService formatter; @SuppressWarnings("javadoc") @Before public void setUp() { formatter = mock(FormatterService.class); setMock(formatter); when(formatter.formatTachoIcon()).thenReturn(MOCKED_URL); } /** * Remove the mocked instance from the class. It is important, because other tests will be confused with the mocked instance. * @throws Exception if the instance could not be accessible */ @After public void resetSingleton() throws Exception { Field instance = FormatterService.class.getDeclaredField("instance"); instance.setAccessible(true); instance.set(null, null); } /** * Set a mock to the {@link FormatterService} instance * Throws {@link RuntimeException} in case if reflection failed, see a {@link Field#set(Object, Object)} method description. * @param mock the mock to be inserted to a class */ private void setMock(FormatterService mock) { Field instance; try { instance = FormatterService.class.getDeclaredField("instance"); instance.setAccessible(true); instance.set(instance, mock); } catch (Exception e) { throw new RuntimeException(e); } } /** * Test method for {@link com.example.DriverSnapshotHandler#getImageURL()}. */ @Test public void testFormatterServiceIsCalled() { DriverSnapshotHandler handler = new DriverSnapshotHandler(); String url = handler.getImageURL(); verify(formatter, atLeastOnce()).formatTachoIcon(); assertEquals(MOCKED_URL, url); } }