Effective Java在单元测试单例上有以下声明
使类成为单例可能会使测试其客户端变得困难,因为除非它实现了作为其类型的接口,否则不可能将模拟实现替换为单例。
有谁可以解释为什么会这样?
答案 0 :(得分:33)
您可以使用反射来重置单个对象,以防止测试相互影响。
@Before
public void resetSingleton() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field instance = MySingleton.class.getDeclaredField("instance");
instance.setAccessible(true);
instance.set(null, null);
}
答案 1 :(得分:9)
模拟需要接口,因为你正在做的是用一个模仿测试所需内容的模板替换真实的底层行为。由于客户端只处理接口引用类型,因此不需要知道实现是什么。
如果没有接口,您无法模拟具体类,因为如果没有测试客户端知道它,您就无法替换该行为。在这种情况下,这是一个全新的课程。
所有类都是如此,Singleton与否。
答案 2 :(得分:7)
我认为这实际上取决于单例访问模式的实现。
例如
MySingleton.getInstance()
时可能会非常难以测试
MySingletonFactory mySingletonFactory = ...
mySingletonFactory.getInstance() //this returns a MySingleton instance or even a subclass
不提供有关其使用单身人士这一事实的任何信息。所以你可以随意更换你的工厂。
注意:单例是由应用程序中该类的一个实例定义的,但是获取或存储它的方式不一定是通过静态方式。
答案 3 :(得分:7)
这很简单。
在单元测试中,您希望隔离您的SUT(您正在测试的类)。 您不想测试一堆类,因为这会破坏 unit - testing 的目的
但并非所有班级都自己做一切,对吗?大多数类使用其他类来完成他们的工作,并且它们在其他类之间进行调解,并添加一些自己的类,以获得最终结果。
关键是 - 你不关心你的SUT课程如何依赖于工作。您关心SUT 如何使用这些类。这就是你存根或模拟你的SUT需要的类的原因。您可以使用这些模拟,因为您可以将它们作为SUT的构造函数参数传递。
对于单身人士 - 不好的是getInstance()
方法是全局可访问的。这意味着您通常从在类中调用它,而不是在以后模拟的界面上依赖。这就是为什么当你想测试你的SUT时,替换它是不可能的。
解决方案不是使用偷偷摸摸的public static MySingleton getInstance()
方法,而是依赖在接口上,您的类需要使用。这样做,你可以随时传入测试双打。
答案 4 :(得分:7)
问题不在于测试单身人士自己;这本书说,如果你试图测试的课程取决于单身,那么你可能会有问题。
除非,即(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
。
答案 5 :(得分:3)
在没有任何外部控制的情况下创建Singleton对象。在同一本书的其他章节中,Bloch建议使用enum
s作为默认的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();
}
测试休闲方法将非常困难。它的执行将取决于它执行的那一天。如果它在工作日执行doSomeWork()
将在周末haveSomeFun()
调用。
对于这种情况,我们需要使用一些重型工具,如PowerMock来拦截GregorianCalendar
构造函数,返回一个模拟,它会返回一个对应于工作日或周末的索引,在两个测试用例中测试两个执行路径leisure
方法。
答案 6 :(得分:3)
it’s impossible to substitute a mock implementation for a singleton
事实并非如此。你可以继承你的单例和setter注入一个mock。或者,您可以使用PowerMock来模拟静态方法。然而,模仿单身人士的需要可能是设计不良的症状。
当滥用变成依赖磁铁时,真正的问题是单身人士。由于它们可以在任何地方访问,因此可以显示更方便地将所需的函数放入其中,而不是委托给适当的类,尤其是对于刚接触OOP的程序员。
可测试性问题现在您有一堆单身人士可供您正在测试的对象访问。即使对象可能只使用单例中的一小部分方法,您仍然需要模拟每个Singleton并找出依赖于哪些方法。具有静态(Monostate模式)的单身人士甚至更糟,因为您必须弄清楚对象之间的哪些交互受到Singleton状态的影响。
小心使用,单身人士和可测试性可以一起发生。例如,在没有DI框架的情况下,您可以使用Singletons作为您的Factories和ServiceLocators,您可以设置注入以为您的端到端测试创建虚假服务层。
答案 7 :(得分:2)
有可能,请参阅the example
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);
}
}
答案 8 :(得分:0)
据我所知,实现Singleton的类不能扩展(超类构造函数总是被隐式调用,而Singleton中的构造函数是私有的)。如果你想模拟一个类,你必须扩展该类。正如你在本案中看到的那样,这是不可能的。
答案 9 :(得分:0)
单例(以及静态方法)的问题在于它很难用模拟的实现替换实际的代码。
例如,请考虑以下代码
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方法编写单元测试并验证是否执行了正确的行为并不容易。
这是因为您无法轻易更改getFeatureFlag
的返回值。
静态方法存在同样的问题 - 用模拟行为替换实际的目标类方法并不容易。
当然,有一些解决方法,如powermock,或方法的依赖注入,或测试中的反射。 但最好不要首先使用单身人士
答案 10 :(得分:0)
使用PowerMock模拟Singleton类(SingletonClassHelper)实例和在task.execute()
中调用的非静态方法(nonStaticMethod)。
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();
}
}
答案 11 :(得分:0)
durian-globals进行lazy double-locked initialization个单例,但还具有一个简单的仅测试API,该API可让您替换单元测试的实现。
答案 12 :(得分:0)
下面是我必须采用一些不可变的 Kotlin 单例来测试它们的解决方案
假设你有一个这样的单例类:
class MySingleton private constructor(
{your dependencies}
) {
companion object {
@JvmStatic
private var INSTANCE: MySingleton? = null
@JvmStatic
fun getInstance(): MySingleton {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: MySingleton(
{your dependencies}
).also {
INSTANCE = it
}
}
}
}
}
您可以在 kotlin junit 测试中执行此操作:
@After
fun after() {
val instance = MySingleton.Companion::class.memberProperties.find {
it.name == "INSTANCE"
}
instance!!.isAccessible = true
instance.javaField!!.set(null, null)
}
您只需要将 kotlin-reflect
工件添加到您的依赖项