Simplicity can be deceptive
There is a seemingly simple solution to test the IntentService
s: just subclass the ServiceTestCase
and you’re done.
But one does not simply test IntentService
s, 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
Intent
s - [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 IntentService
s 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 ReentrantLock
s and Condition
s. 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.
Thanks for this cool post. In your attempt to generalize the method, have you tried to make the CoundDownLatch implement Parcelable? This way it could be passed to the Service through Intent. I tried and failed, but I am a young developer so maybe you’d succeed.
Unfortunately, CountDownLatch is a Java built-in class. This means it cannot be easily made Serializable.
It will fail on testServiceTestCaseSetUpProperly, since the instance of RssFetcherServiceWrapper is null.
Failure in testServiceTestCaseSetUpProperly: junit.framework.AssertionFailedError at android.test.ServiceTestCase.setupService(ServiceTestCase.java:155)
I ran into this problem because the service wrapper was an nested class of the ServiceTestCase implementation. The wrapper needs to be a regular class by itself.
It did not work as a nested class even if it’s static.
As your post helped me a lot, I thought I’d reference here the solution I finally used for a similar problem, using a Looper on the test, for a service that used a BroadcastReceiver to send back the data: http://stackoverflow.com/a/37957010/1361689.