Robolectric测试需要在线程上等待某事

时间:2015-09-22 16:15:20

标签: android multithreading robolectric

我的课这样做:

public void doThing() {
    Doer doer = new Doer();
    Thread thread = new Thread(doer);
    thread.start();
}

' Doer' class是一个内部类:

private class Doer implements Runnable {
    public void run() {
        Intent myIntent = new Intent(mContext, MyService.class);
        mContext.startService(myIntent);

        ...Some more stuff...
    }

这很好用。

我需要使用Robolectric进行测试。当然doThing()会立即返回,我需要让线程有机会在我做之前运行

ShadowApplication.getInstance().getNextStartedService()

我如何等待线程运行?

我试过了:

Robolectric.flushForegroundThreadScheduler();
Robolectric.flushBackgroundThreadScheduler();

并且都没有达到预期效果:它们都在我的意图被发送之前返回。

目前我已经通过在我的测试中进行了一次睡眠来解决这个问题:

Thread.sleep(10);

它可以解决这个问题,但它显然很可怕 - 它是一种等待让我感到悲伤的竞争条件。

5 个答案:

答案 0 :(得分:2)

之前我遇到过这个问题,并且我使用了不同的方法来修复它。 我创建了一个Runnable类的阴影对象,并在阴影构造函数中调用了run。这样,代码将立即执行,使其同步。

使用您的代码作为基础,最终结果应该是类似的。

@Implements(Doer.class)
private class ShadowDoer{
    @RealObject
    private Doer doer;

    // Execute after Doer constructor
    public void __constructor__(<Doer construtor arguments>) {
        doer.run();
    }
}

然后使用@Config(shadows=ShadowDoer.class)

注释您的测试

这样做是在创建一个新对象Doer时,阴影构造函数将直接在主线程中执行并调用run。

我使用的是Robolectric 3.2。

答案 1 :(得分:1)

我使用静态volatile布尔值解决了这个问题,用于通过循环锁定线程。但是对于我的线程实现,还使用回调来表示完成点。

所以在你的情况下,我会在你的doer runnable中添加一个监听器。例如

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

还添加了将侦听器传递给doThing函数的功能。然后在你的测试中做这样的事情。

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

此时,您可以在任何您认为必要的地方添加断言,它们可以来自回调方法,也可以在暂停线程后测试线程计算的结果。

这并不像我希望的那样优雅,但它确实有效,并且允许我为使用线程而不是异步任务的代码部分编写单元测试。

答案 2 :(得分:0)

马克,两个建议:

  1. 测试中只有单个线程(除非是特殊测试)
  2. 单独实例化对象及其用法
  3. 我会做下一步:

    1. 介绍一些负责创建线程的工厂
    2. 在测试中嘲笑
    3. 在测试中捕获runnable并在同一个线程上运行
    4. 验证服务是否已启动
    5. 如果您需要进一步的行动,请告诉我

答案 3 :(得分:0)

这是一个有效的例子。

请注意,它依赖于调用告诉Robolectric启用实时HTTP查询:

FakeHttp.getFakeHttpLayer().interceptHttpRequests(false);

用于管理后台任务完成跟踪的ConditionVariable代码。

我必须将它添加到我的项目的build.gradle文件中(在依赖项块中):

// http://robolectric.org/using-add-on-modules/
compile 'org.robolectric:shadows-httpclient:3.0'
testCompile 'org.robolectric:shadows-httpclient:3.0'

我希望这有帮助!

皮特

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

答案 4 :(得分:0)

您可以使用显示器锁定。

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