上一篇文章介绍了如何测试单例模式(PowerMock!),还有如何对 Android 代码做单元测试(Robolectric!)。现在我们想要测试一个 Service 中的单例应该会很容易了吧?
第一次尝试: 结合 PowerMock 和 Robolectric (1)
// src/PushService
// [PushService.java]
public class PushService extends Service {
public void onMessageReceived(String id, Bundle data){
FooManager.getInstance().receivedMsg(data);
}
}
我试着结合 PowerMock 和 Robolectric 然后写了个测试用例:
// test/PushServiceTest
@RunWith(RobolectricTestRunner.class)
// @RunWith(PowerMockRunner.class)
@PrepareForTest(FooManager.class)
public class FooManagerTest {
@Test
public void testSingleton(){
FooManager mgr = Mockito.mock(FooManager.class);
PowerMockito.mockStatic(FooManager.class);
Mockito.when(FooManager.getInstance()).thenReturn(mgr);
FooManager actual = FooManager.getInstance();
assertEquals(mgr, actual);
}
}
很快,我就发现陷入了两难。即可以用 @RunWith(RobolectricTestRunner.class)
也可以用@RunWith(PowerMockRunner.class)
,但不能两个一起用!一旦可以同时使用这两个语句,意味着可以随意选择使用 Robolectric 或者 PowerMock,但我没办法结合他们。
第二次尝试: 结合 PowerMock 和 Robolectric (2)
我尝试着 Google 可行的解决方案,谢天谢地竟然让我找到了一个。这个方案由 Robolectric 发布在:https://github.com/robolectric/robolectric/wiki/Using-PowerMock
这篇文章建议我们添加如下语句:
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*" })
@PrepareForTest(Static.class)
public class DeckardActivityTest {
...
}
我按照文章说的做了,然后试着运行测试,但还是失败了。这一次的报错信息是:
com.thoughtworks.xstream.converters.ConversionException: Cannot convert type org.apache.tools.ant.Project to type org.apache.tools.ant.Project
---- Debugging information ----
然后我接着 Google,这一次我找到了一个 Github 上的 Issue github.com/Robolectric。在这个 Issue 里有人提到:
很遗憾我们在 10 月以前都没法实现整合 Powermock,但如果有人愿意帮忙修复这个问题我们也非常欢迎。
最好的解决当务之急的办法就是让你的代码变得可测试,这样就不用去模拟静态方法了。
现在我意识到目前还没有能够同时使用 PowerMock 和 Robolectric 的方案。可能在 10 月(2016年)的时候会有,但现在(2016 年 9 月)我必须测试服务里的单例,怎么才能做到?
第三次尝试: 解耦单例
现在我们知道 PowerMock + Robolectric 的方案已经没有希望了,那我们还能不能测试服务里的单例?
还是有办法的,就像前面说的『单例模式被认为是不够好的,因为它使得单元测试和调试变得困难。它需要明确的指定单例对象的类型以至于耦合度过高。』。所以我们希望能创造个实现依赖注入的机会,而不是紧耦合的用具体的单例对象来初始化。
回到我们的例子,如果使用单例,代码应该是这样:
// [PushService.java]
public class PushService extends Service {
public void onMessageReceived(String id, Bundle data){
FooManager.getInstance().receivedMsg(data);
}
}
而使用依赖注入,重写后的代码应该是这样:
// [PushService.java]
public class PushService extends Service {
public FooManager fooManager;
public void onMessageReceived(String id, Bundle data){
fooManager.receivedMsg(data);
}
}
在这个例子里,FooManager
在服务的外层被创建,这样就有了注入或模拟我们自己的实例的机会。这样一来我们的测试代码可以这样写:
@RunWith(RobolectricTestRunner.class) // Use Robolectric to test Service with JUnit
@Config(constants = BuildConfig.class, sdk = 21)
public class PushServiceTest {
@Test
public void testReceivedMessage_Singleton(){
FooManager mgr = mock(FooManager.class);
service.fooManager = mgr;
service.onMessageReceived("23", data);
verify(service.fooManager).receivedMsg(data);
}
}
问题解决了。我们对在服务里初始化对象做了解耦,做到了让测试用例可以模拟单例类的实例,这一点非常重要,为了写出可测试的代码, 必须把对象的实例化和业务逻辑分开。
结论 02
单例模式,由于提供了一个全局的静态方法来创建和获取类的实例,自然阻止了解耦。而我们上面所做的,就是通过把实例化和业务逻辑分开,从而实现了一个单例模式的测试方案。