《Effective Java》中关于单例模式的单元测试有如下表述:
使一个类成为单例可能会使其客户端的测试变得困难,因为除非实现作为其类型的接口,否则无法为单例替代模拟实现。
有人能解释一下为什么吗?
《Effective Java》中关于单例模式的单元测试有如下表述:
使一个类成为单例可能会使其客户端的测试变得困难,因为除非实现作为其类型的接口,否则无法为单例替代模拟实现。
有人能解释一下为什么吗?
@Before
public void resetSingleton() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field instance = MySingleton.class.getDeclaredField("instance");
instance.setAccessible(true);
instance.set(null, null);
}
参考:单元测试单例模式
问题不在于测试单例本身;本书的意思是,如果您要测试的类 依赖于 单例,则可能会出现问题。
除非您 (1) 使单例实现一个接口,并且 (2) 使用该接口将单例注入到您的类中。
例如,单例通常会像这样直接实例化:
public class MyClass
{
private MySingleton __s = MySingleton.getInstance() ;
...
}
现在,MyClass
可能非常难以自动化测试。例如,正如@Boris Pavlović在他的答案中指出的那样,如果单例的行为基于系统时间,那么您的测试现在也依赖于系统时间,而且您可能无法测试依赖于星期几等情况的用例。
但是,如果您的单例“实现了一个充当其类型的接口”,那么只要将其传递进去,您仍然可以使用该接口的单例实现:
public class SomeSingleton
implements SomeInterface
{
...
}
public class MyClass
{
private SomeInterface __s ;
public MyClass( SomeInterface s )
{
__s = s ;
}
...
}
...
MyClass m = new MyClass( SomeSingleton.getInstance() ) ;
从测试的角度来看,你现在不必关心SomeSingleton
是否为单例:你可以传递任何其他实现,包括单例实现,但最有可能使用某种类型的模拟对象来进行控制和测试。
顺便说一句,这不是正确的做法:
public class MyClass
{
private SomeInterface __s = SomeSingleton.getInstance() ;
public MyClass()
{
}
...
}
在运行时,这仍然是一样的,但是在测试时,你现在又依赖于SomeSingleton
。
模拟需要接口,因为你所做的是用一个模仿测试需要的替代品来替换真实的底层行为。由于客户端只处理接口引用类型,所以不需要知道实现是什么。
如果没有接口,就不能模拟具体类,因为你不能替换行为而不让测试客户端知道它。在这种情况下,这是一个全新的类。
对于所有类,无论是Singleton还是其他类,都是如此。
我认为这实际上取决于单例访问模式的实现方式。
例如
MySingleton.getInstance()
在进行测试时可能会非常困难
MySingletonFactory mySingletonFactory = ...
mySingletonFactory.getInstance() //this returns a MySingleton instance or even a subclass
没有提供任何关于使用单例模式的信息。因此,您可以自由地替换工厂。
注意:单例是指应用程序中只有一个该类的实例,但它的获取或存储方式不一定通过静态手段。
这很简单。
在单元测试中,您希望隔离您要测试的类(被测系统)。
您不想测试一堆类,因为那样就失去了单元测试的目的。
但并非所有类都可以独立完成所有任务。大多数类使用其他类来完成工作,并在其他类之间进行协调,同时添加一些自己的功能以获得最终结果。
重点是-您不关心SUT所依赖的类如何工作。您关心的是SUT如何与这些类一起工作。这就是为什么您需要存根或模拟 SUT所需的类。您可以将这些模拟作为构造函数参数传递给SUT。
对于单例模式-坏处在于getInstance()
方法是全局可访问的。这意味着您通常从内部一个类中调用它,而不是依赖于后面可以模拟的接口。这就是为什么当您想要测试SUT时,无法替换它的原因。
解决方案不是使用巧妙的public static MySingleton getInstance()
方法,而是依赖于您的类需要使用的接口。这样做,您就可以在需要时传入测试替身。
单例对象是在外部没有任何控制的情况下创建的。在同一本书的另一章中,Bloch建议使用enum
作为默认的单例实现。让我们看一个例子。
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();
}
测试休闲方法将会非常困难。它的执行将取决于执行时所在的日期。如果在工作日执行,则会调用doSomeWork()
,而在周末则会调用haveSomeFun()
。
对于这种情况,我们需要使用一些重型工具,如PowerMock来拦截GregorianCalendar
构造函数,返回一个模拟对象,该模拟对象将在两个测试用例中分别测试leisure
方法的两个执行路径,并返回对应于工作日或周末的索引。
it’s impossible to substitute a mock implementation for a singleton
有可能,参见示例
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);
}
}
task.execute()
中被调用。 import static org.mockito.Mockito.when;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
@PrepareForTest({ SingletonClassHelper.class })
@RunWith(PowerMockRunner.class)
public class ClassToTest {
@InjectMocks
Task task;
private static final String TEST_PAYLOAD = "data";
private SingletonClassHelper singletonClassHelper;
@Before
public void setUp() {
PowerMockito.mockStatic(SingletonClassHelper.class);
singletonClassHelper = Mockito.mock(SingletonClassHelper.class);
when(SingletonClassHelper.getInstance()).thenReturn(singletonClassHelper);
}
@Test
public void test() {
when(singletonClassHelper.nonStaticMethod(parameterA, parameterB, ...)).thenReturn(TEST_PAYLOAD);
task.execute();
}
}