Thursday, July 5, 2012

Never start or shutdown a service in android.test.ServiceTestCase.setUp() or tearDown()

Setting up your service setUp() or stopping it in tearDown() will break your test-case. Most probably a NullPointerException will be thrown somewhere. Both errors happen independently, but have in essence the same cause: the internal state keeping of ServiceTestCase.

The class Service does not keep track of the service life-cycle state, i.e. whether the service has been created, started, or is about to be destroyed. That is why the ServiceTestCase defines a number of state variables for tracking the state: mServiceAttached, mServiceCreated, mServiceStarted, and mServiceBound. The problem lies with mServiceCreated. Let's check it out in detail.

Calling startService() in setUp()

Starting the service in setUp() might cause an error during testServiceTestCaseSetUpProperly()—and for that matter, during any other of your own tests.

In my case it was only testServiceTestCaseSetUpProperly() that failed with an error. It is the sole test-case defined by ServiceTestCase. Its purpose is to detect improperly configured tests. I does only one thing: calling setupService().

My service was still very rudimentary, resembling the local service example from the API docs of class Service. The test failed during the tearDown(). Per default, tearDown() shuts down the service. This includes invoking onDestroy() on the service. There occurred a NullPointerException. The implementation of my onDestroy() method is as follows:

@Override
public void onDestroy() {
    // Cancel the persistent notification.
    mNM.cancel(NOTIFICATION);
    // Tell the user we stopped.
    Toast.makeText(this, R.string.music_player_stopped, Toast.LENGTH_SHORT).show();
}

Surprisingly the variable mNM (of class NotificationManager) was null, though it was initialized properly in onCreate():

@Override
public void onCreate() {
    mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
    // Display a notification about us starting.  We put an icon in the status bar.
    showNotification();
}

So, when did it went null again? Well, never! Due to calling startService(…) in setUp() it just never had been initialized! It was initialized once, all right. And that was during startService(…). But right after that, testServiceTestCaseSetUpProperly() called setupService(), which will create a new service instance, but will not start it—i.e. onCreate() will not be invoked upon it. So my precious—and initialized—service instance was silently exchanged against an uninitialized one. That alone would not be fatal, were not the state variable ServiceTestCase.mServiceCreated still set to true, thus signaling that onCreate() had been invoked on the service. And because of that, tearDown() will happily invoke onDestroy() upon a service instance whose onCreate() method never had been called. Smells a bit like a bug to me…

Calling shutdownService() in tearDown()

In the case of the shutdown the cause of the error is easy to pinpoint: ServiceTestCase.tearDown() also shuts down the service—and the API docs clearly say so. It is understandable that a second attempt will fail, finding its underlying service gone. Or is it? The error happens in ServiceTestCase.shutdownService(), in line 10 of the following listing:

protected void shutdownService() {
    if (mServiceStarted) {
        mService.stopSelf();
        mServiceStarted = false;
    } else if (mServiceBound) {
        mService.onUnbind(mServiceIntent);
        mServiceBound = false;
    }
    if (mServiceCreated) {
        mService.onDestroy();
    } 
}

The call to ServiceTestCase.tearDown() has just executed this very same code once and afterwards nullified the service instance. However, mServiceCreated is (again) still true, although the service is already destroyed. In fact, this status variable is never re-set to false anywhere in ServiceTestCase. Thus onDestoy() will be invoked on a null reference.

Conclusion

The moral is, never call startService(…) in setUp() and never shutdownService() in tearDown(). It would be nice however, if the official documentation would impress a little more upon that. Also, a little less unforgiving implementation seems possible. Basically that would mean to just re-set mServiceCreated to false in two or so places.