Mark Smith
Mark Smith

Reputation: 909

Robolectric test needs to wait for something on a thread

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

Answers (5)

Laranjeiro
Laranjeiro

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

iamreptar
iamreptar

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

Pete
Pete

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

ocross
ocross

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

Eugen Martynov
Eugen Martynov

Reputation: 20140

Mark, two advices:

  1. only single thread in tests (unless it is special test)
  2. separate instantiation of the object and its usage

I would do next:

  1. Introduce some factory that is responsible for thread creation
  2. Mock it in the test
  3. Capture runnable in test and run it on the same thread
  4. Verify that service was started

Let me know if you need further exaplnations

Upvotes: -1

Related Questions