Thursday 26 October 2017

Testing Service lifecycle method calls

Let's suppose you have created a local Service (implementing the Local Binder Pattern) to perform some tasks for your app, delivering the results back to a connected Activity. 
It is important to properly test the Service to make sure it behaves as you expect. The first thing to do, as the Android documentation suggests, is testing the Service's lifecycle methods.

To be more precise:
  • ensure that the onCreate() is called in response to Context.startService() or Context.bindService(). Similarly, you should ensure that onDestroy() is called in response to Context.stopService(), Context.unbindService(), stopSelf(), or stopSelfResult(). Test that your Service correctly handles multiple calls from Context.startService(). Only the first call triggers Service.onCreate(), but all calls trigger a call to Service.onStartCommand()
If you are not familiar with testing Services you can first check the official Android documentation here:
Testing Your Service


But how do you do that without mixing the code of your Service, implementing you business logic, with the code needed to keep track of the method calls?

@VisibleForTesting

The @VisibleForTesting annotation indicates that an annotated method (or variable) is more visible than normally necessary to make the method testable. This annotation has an optional otherwise argument that lets you designate what the visibility of the method should have been if not for the need to make it visible for testing. Lint uses the otherwise argument to enforce the intended visibility.

You can also specify @VisibleForTesting(otherwise = VisibleForTesting.NONE) to indicate that a method exists only for testing. This form is the same as using @RestrictTo(TESTS). They both perform the same lint check.
For our purpose we can define some static variables, inside our Service, used to keep track of the lifecycle method calls. These variables are annotated with @VisibleForTesting, meaning that they are only used for testing purposes.
// Just for testing.
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
public static int onCreateCalls = 0;
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
public static int onDestroyCalls = 0;
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
public static int onStartCommandCalls = 0;
In the relevant Service lifecycle methods we increment our counters:
@Override
public void onCreate() {
    Log.d(TAG, "RecordingService - onCreate");
    onCreateCalls++;
    // ...
}

@Override
public void onDestroy() {
    Log.d(TAG, "RecordingService - onDestroy");
    onDestroyCalls++;
    // ...
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    onStartCommandCalls++;
    // ...
}

You can see the whole class on GitHub (it's a hybrid Service, both bound and started, used to record audio with a MediaRecorder):
RecordingService.java

Testing the Service

Testing our Service is now very easy. We first define the ServiceTestRule as follows:
@Rule
public final ServiceTestRule mServiceRule = new ServiceTestRule() {
    @Override
    protected void afterService() {
        super.afterService();           
        RecordingService.onCreateCalls = 0;
        RecordingService.onStartCommandCalls = 0;
        RecordingService.onDestroyCalls = 0;
    }
};
Note that in the overriden afterService method, called when the Service is shut down after each test, we reset our counters to 0.

Testing the lifecyle method calls is now very easy:
/*
    Test that the Service's lifecycle methods are called the exact number of times in response
    to binding, unbinding and calls to startService.
*/
@Test
public void testLifecyleMethodCalls() throws TimeoutException {
    // Create the service Intent.
    Intent serviceIntent = RecordingService.makeIntent(InstrumentationRegistry.getTargetContext(), true);

    mServiceRule.startService(serviceIntent);
    IBinder binder = mServiceRule.bindService(serviceIntent);
    RecordingService service = ((RecordingService.LocalBinder) binder).getService();
    mServiceRule.startService(serviceIntent);
    mServiceRule.startService(serviceIntent);

    assertNotNull("Service reference is null", service);
    assertEquals("onCreate called multiple times", 1, RecordingService.onCreateCalls);
    assertEquals("onStartCommand not called 3 times as expected", 3, RecordingService.onStartCommandCalls);

    mServiceRule.unbindService();
    assertEquals("onDestroy not called after unbinding from Service", 1, RecordingService.onDestroyCalls);
}

You can have a look at the entire test class here:
RecordingServiceTest.java

2 comments:

  1. Howdy! Quick question that's completely off topic. Do you know how to make your site mobile friendly? My site looks weird when browsing from my iphone 4. I'm trying to find a template or plugin that might be able to fix this issue. If you have any suggestions, please share. Cheers!
    fax blasting

    ReplyDelete
  2. I’d must test with you here. Which is not one thing I usually do! I get pleasure from reading a publish that can make folks think. Also, thanks for permitting me to remark! apple service provider berlin

    ReplyDelete