【问题标题】:Robolectric test needs to wait for something on a threadRobolectric 测试需要等待线程上的某些东西
【发布时间】:2017-12-05 18:09:55
【问题描述】:

我的班级是这样做的:

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

'Doer' 类是一个内部类:

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

两者都没有达到预期的效果:它们都在我的 Intent 发送之前返回。

目前我已经通过在我的测试中加入睡眠来解决它:

Thread.sleep(10);

它确实成功了,但它显然很可怕 - 这是一个等待让我悲伤的比赛条件。

【问题讨论】:

  • 为什么要使用 Thread 类而不是像 AsyncTask 类这样的包装器? Robolectric 已经有一种机制可以在使用 AsyncTask 时避免并行代码执行。 Robolectric.flushBackgroundThreadScheduler() 只能在您使用容易被 AsyncTask 等 robolectric 遮蔽的机制时产生效果。
  • 因为我需要传入大量参数(未在我的 sn-p 中显示)并且 Thread 实现对于我需要做的事情来说更简单......但是我认为你是对的,赢得 ShadowAsyncTask 的好处——这就是我所使用的。

标签: android multithreading robolectric


【解决方案1】:

我之前遇到过这个问题,我使用了不同的方法来解决它。 我创建了我的 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。

【讨论】:

    【解决方案2】:

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

    因此,在您的情况下,我会在您的 doer 可运行文件中添加一个侦听器。例如

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

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

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

    【讨论】:

      【解决方案3】:

      这是一个工作示例。

      请注意,它依赖于一个调用来告诉 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】:

        您可以使用监视器锁。

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

        【讨论】:

          【解决方案5】:

          马克,两个建议:

          1. 测试中只有单线程(除非是特殊测试)
          2. 对象的单独实例化及其用法

          接下来我会做:

          1. 介绍一些负责创建线程的工厂
          2. 在测试中模拟它
          3. 在 test 中捕获 runnable 并在同一线程上运行它
          4. 验证服务是否已启动

          如果您需要进一步的解释,请告诉我

          【讨论】:

          • 感谢您的回答。正如 Nenick 所建议的,我没有将它分成两个类,而是转移到了 AsyncTask。
          • 由你决定,但对我来说,你并没有做得更好
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-02-27
          • 1970-01-01
          • 2013-07-15
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多