One does not simply test the IntentServices

Simplicity can be deceptive

There is a seemingly simple solution to test the IntentServices: just subclass the ServiceTestCase and you’re done.

But one does not simply test IntentServices, due to thread synchronization troubles.

JUnit test cases run in the main application thread, which is fine: the usual testing code gets executed sequentially and there is either green or red bar in the end.

However, IntentService, being a simple solution to multithreading issues, runs all onHandleIntent() calls in the separate worker thread.

Therefore, the usual running sequence is:

  • [main thread] creates a new instance of IntentService
  • [main thread] starts it via startService()
  • [worker thread] starts worker thread
  • [main thread] do some Assert.assertXXX() assertions
  • [worker thread] waits for incoming Intents
  • [main thread] terminates, thus ending the unit test

The thing is that the worker thread may either not start at all or it does not get to process any intents, since the test is already done.

Solutions (none of them quite perfect)

There have been some attempts at solving this problem. Basically, we need to pause the unit test thread until the worker thread finishes its job in.

Only when I sleep…

Use TimeUnit.SECONDS.sleep(X) in your unit tests, where X is some arbitrary, experimentally deduced number. Simple solution, but may break on occasions.

public void testHandleIntent() throws InterruptedException {
    try {
        startService(new Intent(getSystemContext(), RssFetcherService.class));
        TimeUnit.SECONDS.sleep(X);
    } catch (InterruptedException e) {
        fail("Waiting for the worker thread took too long.");
    }
}

“My little android warehouse” solution

Federico Paolinelli offers some solution to this issue. Honestly, I wasn’t able to grasp his ideas (mostly due to incomplete example), but on the other hand, he treads the way via thread synchronization techniques — by using semaphores.

Antoine Martin Solution

Antoine Martin offers improved solution based on countdown latches. Moreover, he provides a complete code.

However, his solution did not work for me for the most simple IntentServices that do not use result notification. It seems to suffer from the same trouble: if the worker thread does not notify the test thread, you are stuck as I was in the beginning.

On the other hand, it seems to work when the intent service notifies that the work is done: however, you need to modify your intent service class.

Cryptic Stackoverflow.com ideas

There is a cryptic suggestion on Stackoverflow pointing to ReentrantLocks and Conditions. Another thread synchronization mystery, this time without any code, but it may work.

Lots-o’-java Solution

So far, all ideas suggested some kind of thread synchronization. In our solution we will use the simple idea of CountDownLatch.

Both threads — test and the onHandleIntent() in IntentService — will share the latch initialized with 1.

Test thread will start the testXXX() code and then will block until the latch turns zero.

In the worker thread, when the work is done, we will decrease the latch (setting it to zero), thus unblocking the test thread and succeeding/failing the unit test.

Wrapping the service

We need to modify the intent service behaviour by extending the onHandleIntent() method: we need to process the intent and then to decrease the latch.

To do this, we subclass our intent service. Let’s assume we are testing the RssFetcherService. Then:

package com.example.ereses.test;

import java.util.concurrent.CountDownLatch;

import android.content.Intent;

import com.example.ereses.RssFetcherService;

public class RssFetcherServiceWrapper extends RssFetcherService {
    private CountDownLatch latch;

    @Override
    protected void onHandleIntent(Intent intent) {
        super.onHandleIntent(intent);
        latch.countDown();
    }

    public void setLatch(CountDownLatch latch) {
        this.latch = latch;
    }

}

Creating the unit test.

The unit test class will derive from the usual ServiceTestCase, while testing our subclassed latch-decrementing service:

package com.example.ereses.test;

import java.util.concurrent.CountDownLatch;

import android.content.Intent;
import android.test.ServiceTestCase;

import com.example.ereses.RssFetcherService;

public class RssFetcherServiceTest extends ServiceTestCase<RssFetcherServiceWrapper> {
    public static final String LOG_TAG = "RssFetcherServiceTest";
    private CountDownLatch latch;

    public RssFetcherServiceTest() {        
        super(RssFetcherServiceWrapper.class);
    }

    @Override
    protected void setupService() {
        super.setupService();

        latch = new CountDownLatch(1);
        getService().setLatch(latch);
    }

    public void testHandleIntent() throws InterruptedException {
        startService(new Intent(getSystemContext(), RssFetcherService.class));
        latch.await();
    }

}

In the setupService() we prepare the latch instance and associate it with our service under test. Then, within test methods we execute the testing code and then await() until the latch is set to zero.

Note that the latch is set to one: this means that the onHandleIntent() will be executed just once what corresponds to the single Intent being processed. When you want to process multiple intents, you need to increase the initial latch value.

Generalizing the solution (or not being able to do that)

This is quite lot of repetitive code. Each test requires a companion subclass and a boilerplate code in the ServiceTestCase.

Unfortunately, this cannot be generalized, since we do not have any control of the IntentService instantiation. This is done in the setupService() of the ServiceTestCase class and definitely cannot be easily customized. (See the Android sources).

Delegate design pattern? No.

You cannot create a proper wrapping service (using the delegate pattern), since there is a lot of internal workings that don’t go well with this pattern.

Copy-the-source? No.

I even tried to completely Ctrl-C, Ctrl-V the ServiceTestCase class and modifying it, however it uses some Android internal API (the Service#attach() method) which is prohibited in the client code:

The method attach(Context, null, String, null, Application, null) is undefined for the type T

Dynamic proxies? No.

I have even looked on the java.lang dynamic proxies but that failed pretty quickly — since the Service is a abstract class, it cannot be proxied: this works with interfaces only.

Making the general TestableIntentService

You may simplify the idea by creating something like TestableIntentService that allows to set the latch, you will save some code in the testing department while putting some boilerplate into your production code.

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *

Môžete použiť tieto HTML značky a atribúty: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">