如何测试 Android Service 里的 Singleton (1)

本文讲的是如何测试 Android Service 里的 Singleton (1),


最近我遇到个大麻烦:如何测试服务里的单例模式?最终我解决了这个问题。而且我觉得整个解决问题的过程是一个绝好的向读者清楚的解释单元测试的机会。限于篇幅,本文是第一篇文章,后面我会再写一篇。

我们的服务

// [PushService.java]
public class PushService extends Service {
    public void onMessageReceived(String id, Bundle data){
        FooManager.getInstance().receivedMsg(data);
    }
}

FooManager 是一个实例:

// [FooManager.java]
public class FooManager {
    private static FooManager instance = new FooManager();

    private FooManager(){}

    public static FooManager getInstance(){
        return instance;
    }

    public void receivedMsg(Bundle data){
    }
}

我们应该怎么测试 PushService?

显然,我们想确保 FooManager 会调用 receiveMsg(),所以我们想要的应该是像下面这样:

verify(fooManager).receiveMsg(data);

只要是了解 Mockito 的开发者都知道,当我们调用 verify(fooManager) 时必须使 fooManager 先成为一个模拟对象;否则,程序会抛出异常:org.mockito.exception.misusing.NotAMockException

所以我们得先模拟一个 FooManager 的实例。现在我把测试步骤分解成两个小的测试:

  1. 模拟一个单例
  2. 在服务里模拟一个单例

模拟单例(1)

第一步 : 用 Mockito 模拟 FooManager (失败)

首先写一个测试用例:

public class FooManagerTest {
    @Test
    public void testSingleton(){
        FooManager mgr = Mockito.mock(FooManager.class);
        Mockito.when(FooManager.getInstance()).thenReturn(mgr);

        FooManager actual = FooManager.getInstance();
        assertEquals(mgr, actual);
    }
}

运行这个用例时程序抛出了异常:

org.mockito.exceptions.misusing.MissingMethodInvocationException:
when() requires an argument which has to be 'a method call on a mock'.
For example:
    when(mock.getArticles()).thenReturn(articles);
Also, this error might show up because:
1\. you stub either of: final/private/equals()/hashCode() methods.
   Those methods *cannot* be stubbed/verified.
   Mocking methods declared on non-public parent classes is not supported.
2\. inside when() you don't call method on mock but on some other object.

这是因为 Mockito 不能模拟一个静态方法,在这个例子里就是 getInstance() 方法。

第二步 : 使用 PowerMock

还好我知道 PowerMock 可以模拟静态方法,所以我想换到 PowerMock 试试。

@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);
    }

}

是的,我成功了。但必须要注意上面这些代码只有在你的项目是个纯 Java 项目而不是 Android 项目时才能成功。如果想要测试 Android 项目的代码,还会遇到一些其他的问题。

测试 Android 代码

第三步 : 用单元测试来测试 Android 代码

你也许会想到,因为 Android 项目也是用 Java 写的,所以应该也可以在 $module$/src/test 目录里写单元测试的用例。

但是真的可以么?我们来看一个用 JUnit Test 来测试 Android 库代码的例子。

    @Test
    public void testAndroidCode(){
        instance.setArgu(argu);

        instance.doSomething();
        verify(argu).isCalled();
    }

然而,你可能会遇到一个报错: java.lang.NoClassDefFoundError: org/apache/http/cookie/Cookie

除此之外,也可能找不到其他的类,比如 android/util/Logandroid/content/Context 等等。

之所以会报 NoClassDefFoundError 错误是因为 JUnit 运行在 JVM 环境,也就是说 JUnit 没有 Android 运行环境。

其实有一个官方的 Android 环境下的测试方案:Instrumentation 测试

但这并不是我们真正想要的。每次运行 Instrumentation 测试,都必须构建整个项目并把 APK 文件推送到手机设备或者模拟器里。所以,这样测试会很慢。并不像 JUnit 那样可以直接在电脑上运行(PC/Mac/Linux)而且并不需要 Android 运行环境。结果就是在电脑上运行 JUnit 测试会比 Instrumentation 测试快得多。

有没有一个方案既包含 Android 环境又能在电脑上运行还能快速的执行测试?当然有,不然我写这篇文章干嘛,答案就是Robolectric!

第四步 : Robolectric

前面已经说过,在 Android 模拟器或者物理设备上运行测试是很慢的。构建、部署和启动应用通常要花费 1 分钟或者更久,这样没办法做 TDD(Test-driven development 测试驱动的开发)。

Robolectric 是一个让你可以直接在 IDE 里运行 Android 测试的框架。

Robolectric 做了什么?这有点复杂,不过可以简单的认为 Robolectric 封装了一个 Android.jar 文件在其内部。这样就拥有了 Android 运行环境,因此也就可以在电脑上运行 Android 代码的测试。

下面是一个 Robolectric 的例子:

@RunWith(RobolectricTestRunner.class)
public class MyActivityTest {

  @Test
  public void clickingButton_shouldChangeResultsViewText() throws Exception {
    MyActivity activity = Robolectric.setupActivity(MyActivity.class);

    Button button = (Button) activity.findViewById(R.id.button);
    TextView results = (TextView) activity.findViewById(R.id.results);

    button.performClick();
    assertThat(results.getText().toString()).isEqualTo("Robolectric Rocks!");
  }
}

回到主题,在 Robolectric 的帮助下,我们终于可以直接在电脑环境里测试自己的服务,而且还很快。

结论 01

我介绍了如何使用 Robolectric 来快速的测试 Android 代码,以及如何在 Java 环境里模拟单例模式。

但我必须得提醒一下,目前我们仍然无法在 Android 环境里成功的模拟单例模式。我将在下一篇文章里讨论如何解决这个问题。






原文发布时间为:2016年10月12日


本文来自合作伙伴掘金,了解相关信息可以关注掘金网站。

时间: 2024-11-01 11:04:46

如何测试 Android Service 里的 Singleton (1)的相关文章

如何测试 Android Service 里的 Singleton (2)

本文讲的是如何测试 Android Service 里的 Singleton (2), 上一篇文章介绍了如何测试单例模式(PowerMock!),还有如何对 Android 代码做单元测试(Robolectric!).现在我们想要测试一个 Service 中的单例应该会很容易了吧? 第一次尝试: 结合 PowerMock 和 Robolectric (1) // src/PushService // [PushService.java] public class PushService exte

android service里的全局变量值被清空。谁遇到过?跪求。。。。

问题描述 android service里的全局变量值被清空.谁遇到过?跪求.... service是通过activity启动的.service没有被干掉. 解决方案 估计是你的代码有问题,或者是手机有问题.更可能是被kill掉又恢复后

5、Android Service测试

如果你在应用中使用了Service,你应该来测试这个Service来确保它正常工作.你可以创建仪表测试来验证Service的行为是否正确:比如,service保存和返回有效的数值并正常的处理数据. Android Testing Support Library在隔离状态下测试你的Service对象的API.ServiceTestRule类会在你的单元测试类运行之前就启动service,在测试完成之后关闭服务.通过这个规则,你可以保证service会在你的测试方法运行之前建立. ServiceTe

Android Service生命周期 Service里面的onStartCommand()方法详解

在Demo上,Start一个Service之后,执行顺序:onCreate - > onStartCommand 然后关闭应用,会重新执行上面两步. 但是把代码拷贝到游戏工程发现,关闭游戏后,只执行了onStart,却没有执行onStartCommand! 查找到下面的文章: [plain] view plain copy   Service里面的onStartCommand()方法详解      启动service的时候,onCreate方法只有第一次会调用,onStartCommand和on

Android Service服务详细介绍及使用总结_Android

Android Service服务详解 一.Service简介        Service是android 系统中的四大组件之一(Activity.Service.BroadcastReceiver. ContentProvider),它跟Activity的级别差不多,但不能页面显示只能后台运行,并且可以和其他组件进行交互.service可以在很多场合的应用中使用,比如播放多媒体的时候用户启动了其他Activity这个时候程序要在后台继续播放,比如检测SD卡上文件的变化,再或者在后台记录你地理

Android Service服务详细介绍及使用总结

Android Service服务详解 一.Service简介 Service是android 系统中的四大组件之一(Activity.Service.BroadcastReceiver. ContentProvider),它跟Activity的级别差不多,但不能页面显示只能后台运行,并且可以和其他组件进行交互.service可以在很多场合的应用中使用,比如播放多媒体的时候用户启动了其他Activity这个时候程序要在后台继续播放,比如检测SD卡上文件的变化,再或者在后台记录你地理信息位置的改变

Android Service服务(三) bindService与remoteService

一.bindService简介 bindService是绑定Service服务,执行service服务中的逻辑流程. service通过 Context.startService()方法开始,通过Context.stopService()方法停止:也可以通过Service.stopSelf()方法或者 Service.stopSelfResult()方法来停止自己.只要调用一次stopService()方法便可以停止服务,无论之前它被调用了多少次的 启动服务方法. 客户端建立一个与Service

Android Service服务(二) BroadcastReceiver

一. BroadcastReceiver简介 BroadcastReceiver,用于异步接收广播Intent,广播Intent是通过调用 Context.sendBroadcast()发送.BroadcastReceiver()接收. 广播Intent的发送是通过调用Context.sendBroadcast(). Context.sendOrderedBroadcast().Context.sendStickyBroadcast()来实现的.通常一个广播Intent可以被订阅了此Intent

Android Service中方法使用详细介绍

  Android Service中方法使用详细介绍 在Android中,Activity主要负责前台页面的展示,Service主要负责需要长期运行的任务.例如,一个从service播放音乐的音乐播放器,应被设置为前台运行,因为用户会明确地注意它的运行.在状态栏中的通知可能会显示当前的歌曲并且允许用户启动一个activity来与音乐播放器交互. Service的两种实现形式 1.非绑定 通过调用应用程序组件(例如Activity)的startService()方法来启动一个服务.一旦启动,服务就