Robolectric test needs to wait for something on a thread

家住魔仙堡 提交于 2019-12-23 12:05:37

问题


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.


回答1:


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.




回答2:


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.




回答3:


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




回答4:


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]);
}



回答5:


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()));
}


来源:https://stackoverflow.com/questions/32721831/robolectric-test-needs-to-wait-for-something-on-a-thread

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!