单身和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); } }