Reputation: 909
My class does this:
public void doThing() {
Doer doer = new Doer();
Thread thread = new Thread(doer);
thread.start();
}
The 'Doer' class is an inner class:
private class Doer implements Runnable {
public void run() {
Intent myIntent = new Intent(mContext, MyService.class);
mContext.startService(myIntent);
...Some more stuff...
}
This works fine.
I need to test this using Robolectric. Naturally doThing()
returns immediately and I need to give the thread a chance to run before I do
ShadowApplication.getInstance().getNextStartedService()
How can I wait for the thread to run?
I have tried:
Robolectric.flushForegroundThreadScheduler();
Robolectric.flushBackgroundThreadScheduler();
and neither has the desired effect: they both return before my Intent has been sent.
At the moment I have worked around it by putting a sleep into my test:
Thread.sleep(10);
and it does the trick, but it's clearly horrible - it's a race condition waiting to cause me grief.
Upvotes: 6
Views: 11386
Reputation: 2622
I had this problem before and I used a different approach to fix it. I created a shadow object of my Runnable class and invoked run in the shadow constructor. This way, the code will execute straight away making it synchronous.
Using your code as basis, the end result should be something like.
@Implements(Doer.class)
private class ShadowDoer{
@RealObject
private Doer doer;
// Execute after Doer constructor
public void __constructor__(<Doer construtor arguments>) {
doer.run();
}
}
Then annotate your test with @Config(shadows=ShadowDoer.class)
What this does is when you create a new object Doer, the shadow constructor will execute and invoke run directly in the main thread.
I used Robolectric 3.2.
Upvotes: 3
Reputation: 1451
You can use a monitor lock.
private final Object monitorLock = new Object();
private final AtomicBoolean isServiceStarted = new AtomicBoolean(false);
@Test
public void startService_whenRunnableCalled(final Context context) {
Thread startServiceThread = new Thread(new Runnable() {
@Override
public void run() {
context.startService(new Intent(context, MyService.class));
isServiceStarted.set(true);
// startServiceThread acquires monitorLock.
synchronized (monitorLock) {
// startServiceThread moves test thread to BLOCKING
monitorLock.notifyAll();
}
// startServiceThread releases monitorLock
// and test thread is moved to RUNNING
}
});
startServiceThread.start();
while (!isServiceStarted.get()) {
// test thread acquires monitorLock.
synchronized (monitorLock) {
// test thread is now WAITING, monitorLock released.
monitorLock.wait();
// test thread is now BLOCKING.
// When startServiceThread releases monitorLock,
// test thread will re-acquire it and be RUNNING.
}
// test thread releases monitorLock
}
Intent intent = ShadowApplication.getInstance().getNextStartedService();
assertThat(intent.getComponent().getClassName(), is(MyService.class.getName()));
}
Upvotes: 0
Reputation: 794
Here is a working example.
Note that it relies on a call to tell Robolectric to enable live HTTP queries:
FakeHttp.getFakeHttpLayer().interceptHttpRequests(false);
The code a ConditionVariable to manage tracking of completion of the background task.
I had to add this to my project's build.gradle file (in the dependencies block):
// http://robolectric.org/using-add-on-modules/
compile 'org.robolectric:shadows-httpclient:3.0'
testCompile 'org.robolectric:shadows-httpclient:3.0'
I hope this helps!
Pete
// This is a dummy class that makes a deferred HTTP call.
static class ItemUnderTest {
interface IMyCallbackHandler {
void completedWithResult(String result);
}
public void methodUnderTestThatMakesDeferredHttpCall(final IMyCallbackHandler completion) {
// Make the deferred HTTP call in here.
// Write the code such that completion.completedWithResult(...) is called once the
// Http query has completed in a separate thread.
// This is just a dummy/example of how things work!
new Thread() {
@Override
public void run() {
// Thread entry point.
// Pretend our background call was handled in some way.
completion.completedWithResult("Hello");
}
}.start();
}
}
@org.junit.Test
public void testGetDetailedLatestResultsForWithInvalidEmailPasswordUnit_LiveQuery() throws Exception {
// Tell Robolectric that we want to perform a "Live" test, against the real underlying server.
FakeHttp.getFakeHttpLayer().interceptHttpRequests(false);
// Construct a ConditionVariable that is used to signal to the main test thread,
// once the background work has completed.
final ConditionVariable cv = new ConditionVariable();
// Used to track that the background call really happened.
final boolean[] responseCalled = {false};
final ItemUnderTest itemUnderTest = new ItemUnderTest();
// Construct, and run, a thread to perform the background call that we want to "wait" for.
new Thread() {
@Override
public void run() {
// Thread entry point.
// Make the call that does something in the background...!
itemUnderTest.methodUnderTestThatMakesDeferredHttpCall(
new ItemUnderTest.IMyCallbackHandler() {
@Override
public void completedWithResult(String result) {
// This is intended to be called at some point down the line, outside of the main thread.
responseCalled[0] = true;
// Verify the result is what you expect, in some way!
org.junit.Assert.assertNotNull(result);
// Unblock the ConditionVariable... so the main thread can complete
cv.open();
}
}
);
// Nothing to do here, in particular...
}
}.start();
// Perform a timed-out wait for the background work to complete.
cv.block(5000);
org.junit.Assert.assertTrue(responseCalled[0]);
}
Upvotes: 0
Reputation: 613
I worked around this problem with a static volatile boolean used to lock up the thread with a loop. However for my thread implementation also used callbacks to signify completion points.
So in your case I would add a listener into your doer runnable. For Example
private class Doer implements Runnable {
Interface FinishedListener {
finished();
}
FinishedListener mListener;
public Doer(FinishedListener listener) {
mListener = listener;
}
public void run() {
Intent myIntent = new Intent(mContext, MyService.class);
mContext.startService(myIntent);
...Some more stuff...
mListener.finished();
}
Also add the ability to pass along the listener to the doThing function. Then within your test do something like this.
static volatile boolean sPauseTest;
@Test
public void test_doThing() {
sPauseTest = true;
doThing(new FinishedListener() {
@Override
public void finished() {
sPauseTest = false;
}
});
while (sPauseTest) {
try {
Thread.sleep(100);
} catch(InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
At this point you can add asserts wherever you deem necessary they can be from both the callback method or after the pausing of the threads to test results of the threads calculations.
This is not as elegant as I would like it to be but it does work and allows me to write unit tests for my portions of code that use threads and not async tasks.
Upvotes: 2
Reputation: 20140
Mark, two advices:
I would do next:
Let me know if you need further exaplnations
Upvotes: -1