Android RxJava 2 JUnit测试 - android.os.Looper中的getMainLooper未模拟运行时异常

73

当我尝试运行一个使用observeOn(AndroidSchedulers.mainThread())的 presenter 的 JUnit 测试时,我遇到了 RuntimeException。

由于它们是纯 JUnit 测试,而不是 Android 插桩测试,因此它们无法访问 Android 依赖项,导致在执行测试时遇到以下错误:

java.lang.ExceptionInInitializerError
    at io.reactivex.android.schedulers.AndroidSchedulers$1.call(AndroidSchedulers.java:35)
    at io.reactivex.android.schedulers.AndroidSchedulers$1.call(AndroidSchedulers.java:33)
    at io.reactivex.android.plugins.RxAndroidPlugins.callRequireNonNull(RxAndroidPlugins.java:70)
    at io.reactivex.android.plugins.RxAndroidPlugins.initMainThreadScheduler(RxAndroidPlugins.java:40)
    at io.reactivex.android.schedulers.AndroidSchedulers.<clinit>(AndroidSchedulers.java:32)
    …
Caused by: java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details.
    at android.os.Looper.getMainLooper(Looper.java)
    at io.reactivex.android.schedulers.AndroidSchedulers$MainHolder.<clinit>(AndroidSchedulers.java:29)
    ...


java.lang.NoClassDefFoundError: Could not initialize class io.reactivex.android.schedulers.AndroidSchedulers
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    …
10个回答

87

这个错误是由于AndroidSchedulers.mainThread()返回的默认调度程序是LooperScheduler的一个实例,并依赖于在JUnit测试中不可用的Android依赖项所致。

我们可以通过在测试运行之前使用不同的调度程序初始化RxAndroidPlugins来避免此问题。您可以在@BeforeClass方法中执行以下操作:

@BeforeClass
public static void setUpRxSchedulers() {
    Scheduler immediate = new Scheduler() {
        @Override
        public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) {
            // this prevents StackOverflowErrors when scheduling with a delay
            return super.scheduleDirect(run, 0, unit);
        }

        @Override
        public Worker createWorker() {
            return new ExecutorScheduler.ExecutorWorker(Runnable::run);
        }
    };

    RxJavaPlugins.setInitIoSchedulerHandler(scheduler -> immediate);
    RxJavaPlugins.setInitComputationSchedulerHandler(scheduler -> immediate);
    RxJavaPlugins.setInitNewThreadSchedulerHandler(scheduler -> immediate);
    RxJavaPlugins.setInitSingleSchedulerHandler(scheduler -> immediate);
    RxAndroidPlugins.setInitMainThreadSchedulerHandler(scheduler -> immediate);
}

或者您可以创建一个自定义的TestRule,这将允许您在多个测试类之间重用初始化逻辑。

public class RxImmediateSchedulerRule implements TestRule {
    private Scheduler immediate = new Scheduler() {
        @Override
        public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) {
            // this prevents StackOverflowErrors when scheduling with a delay
            return super.scheduleDirect(run, 0, unit);
        }

        @Override
        public Worker createWorker() {
            return new ExecutorScheduler.ExecutorWorker(Runnable::run);
        }
    };

    @Override
    public Statement apply(final Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                RxJavaPlugins.setInitIoSchedulerHandler(scheduler -> immediate);
                RxJavaPlugins.setInitComputationSchedulerHandler(scheduler -> immediate);
                RxJavaPlugins.setInitNewThreadSchedulerHandler(scheduler -> immediate);
                RxJavaPlugins.setInitSingleSchedulerHandler(scheduler -> immediate);
                RxAndroidPlugins.setInitMainThreadSchedulerHandler(scheduler -> immediate);

                try {
                    base.evaluate();
                } finally {
                    RxJavaPlugins.reset();
                    RxAndroidPlugins.reset();
                }
            }
        };
    }
}

然后您可以将其应用于您的测试类

public class TestClass {
    @ClassRule public static final RxImmediateSchedulerRule schedulers = new RxImmediateSchedulerRule();

    @Test
    public void testStuff_stuffHappens() {
       ...
    }
}

这两种方法都可以确保在任何测试执行之前和在访问AndroidSchedulers之前覆盖默认的RxJava调度程序。

通过立即调度程序覆盖RxJava调度程序进行单元测试还可以确保在测试代码中使用的RxJava使用方式以同步方式运行,这将使编写单元测试变得更加容易。

来源:
https://www.infoq.com/articles/Testing-RxJava2 https://medium.com/@peter.tackage/overriding-rxandroid-schedulers-in-rxjava-2-5561b3d14212


请检查我的答案,如果遇到StackOverflowError可能需要进行小的更改。 - AA_PV
更新: 您可以使用RxJavaHooks方法来设置调度程序。 此外,您还可以使用TestScheduler,以及简单地使用Schedulers.immediate()。 - Nelson Ramirez
@NelsonRamirez 我相信 RxJavaHooks 在 RxJava 2 中已被移除,其功能现在已合并到 RxJavaPlugins 中。 - starkej2
我在 Kotlin 项目中做了类似的事情,它可以工作,但不知为何在 Java 项目中,只有当你通过 ./gradlew test 运行时单元测试才会失败,但在 Android Studio 中,在测试类名称旁边的 gutter 中点击运行,则测试通过。我认为它以相同的方式运行测试。是否有人知道这个细微差别? - Etienne Lawlor

53

我刚刚添加了

RxAndroidPlugins.setInitMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline());

@Before注解方法中。


1
这个方法在我只有一个使用Schedulers的测试时有效,但如果我有多个Schedulers,则会失败。不过,被接受的答案是有效的。 - Subhrajyoti Sen
1
在添加以下规则后,这对我起作用了。@get:Rule var rule: TestRule = InstantTaskExecutorRule() - Sam
1
这对我来说立即起作用,无需其他任何操作。谢谢! - A. Steenbergen

22

在测试LiveData时,如果被测试的类既有后台线程又有LiveData,则需要除RxImmediateSchedulerRule之外还需要这个InstantTaskExecutorRule

@RunWith(MockitoJUnitRunner::class)
class MainViewModelTest {

    companion object {
        @ClassRule @JvmField
        val schedulers = RxImmediateSchedulerRule()
    }

    @Rule
    @JvmField
    val rule = InstantTaskExecutorRule()

    @Mock
    lateinit var dataRepository: DataRepository

    lateinit var model: MainViewModel

    @Before
    fun setUp() {
      model = MainViewModel(dataRepository)
    }

    @Test
    fun fetchData() {
      //given    
      val returnedItem = createDummyItem()    
      val observer = mock<Observer<List<Post>>>()    
      model.getPosts().observeForever(observer)    
      //when    
      liveData.value = listOf(returnedItem)    
      //than    
      verify(observer).onChanged(listOf(Post(returnedItem.id, returnedItem.title, returnedItem.url)))
    }

}

参考资料: https://pbochenski.pl/blog/07-12-2017-testing_livedata.html


Kotlin和@JvmField,你救了我 :) - Ahmed Mostafa
1
不要忘记检查你的依赖项是否全部为 androidx 或者全部为 android.arch/com.android。你不能混合使用,否则你会浪费很多时间去思考为什么它不起作用 =) - JDenais
仅使用 @Rule @JvmField val rule = InstantTaskExecutorRule() 就解决了我的问题。谢谢! - fahrizal89

17

参考 @starkej2 的回答,并进行了一些修改,对于 Kotlin 开发者的正确答案如下:

  1. 创建 RxImmediateSchedulerRule.kt 类:

,

import io.reactivex.Scheduler
import io.reactivex.android.plugins.RxAndroidPlugins
import io.reactivex.internal.schedulers.ExecutorScheduler
import io.reactivex.plugins.RxJavaPlugins
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import java.util.concurrent.Executor

class RxImmediateSchedulerRule : TestRule {
    private val immediate = object : Scheduler() {
        override fun createWorker(): Worker {
            return ExecutorScheduler.ExecutorWorker(Executor { it.run() })
        }
    }

    override fun apply(base: Statement, description: Description): Statement {
        return object : Statement() {
            @Throws(Throwable::class)
            override fun evaluate() {
                RxJavaPlugins.setInitIoSchedulerHandler { immediate }
                RxJavaPlugins.setInitComputationSchedulerHandler { immediate }
                RxJavaPlugins.setInitNewThreadSchedulerHandler { immediate }
                RxJavaPlugins.setInitSingleSchedulerHandler { immediate }
                RxAndroidPlugins.setInitMainThreadSchedulerHandler { immediate }

                try {
                    base.evaluate()
                } finally {
                    RxJavaPlugins.reset()
                    RxAndroidPlugins.reset()
                }
            }
        }
    }
}
  1. 在你的测试类中,创建 schedulers 类规则:

class TestViewModelTest {

companion object {
   @ClassRule
   @JvmField
   val schedulers = RxImmediateSchedulerRule()
}

@Before
fun setUp() {
    //your setup code here
}

@Test
fun yourTestMethodHere{}
}

9

正如Peter Tackage在Medium文章中的建议,您可以自己注入Schedulers。

我们都知道直接调用静态方法可能会导致难以测试的类,如果您使用依赖注入框架(如Dagger 2),则注入Schedulers可能特别容易。示例如下:

在您的项目中定义一个接口:

public interface SchedulerProvider {
    Scheduler ui();
    Scheduler computation();
    Scheduler io();
    Scheduler special();
    // Other schedulers as required…
}

定义一个实现:

final class AppSchedulerProvider implements SchedulerProvider {
    @Override 
    public Scheduler ui() {
        return AndroidSchedulers.mainThread();
    }
    @Override 
    public Scheduler computation() {
        return Schedulers.computation();
    }
    @Override 
    public Scheduler io() {
        return Schedulers.io();
    }
    @Override 
    public Scheduler special() {
        return MyOwnSchedulers.special();
    }
}

现在,不再直接使用对调度器的引用,像这样:
 bookstoreModel.getFavoriteBook()
               .map(Book::getTitle)
               .delay(5, TimeUnit.SECONDS)
               .observeOn(AndroidSchedulers.mainThread())
               .subscribe(view::setBookTitle));

您使用接口引用:
bookstoreModel.getFavoriteBook()
          .map(Book::getTitle)
          .delay(5, TimeUnit.SECONDS, 
                 this.schedulerProvider.computation())
          .observeOn(this.schedulerProvider.ui())
          .subscribe(view::setBookTitle));

现在针对您的测试,您可以定义一个类似这样的TestSchedulersProvider:
public final class TestSchedulersProvider implements SchedulerProvider {

      @Override
      public Scheduler ui() {
          return new TestScheduler();
      }

      @Override
      public Scheduler io() {
          return Schedulers.trampoline(); //or test scheduler if you want
      }

      //etc
}

现在,当你想在单元测试中使用 TestScheduler 时,你拥有所有的优势。这在你想测试延迟的情况下非常方便:

@Test
public void testIntegerOneIsEmittedAt20Seconds() {
    //arrange
    TestObserver<Integer> o = delayedRepository.delayedInt()
            .test();

    //act
    testScheduler.advanceTimeTo(20, TimeUnit.SECONDS);

    //assert
    o.assertValue(1);
}

否则,如果您不想使用注入的调度程序,可以使用其他方法中提到的静态钩子来完成,使用lambda表达式即可:
@Before
public void setUp() {
    RxAndroidPlugins.setInitMainThreadSchedulerHandler(h -> Schedulers.trampoline());
    RxJavaPlugins.setIoSchedulerHandler(h -> Schedulers.trampoline());
//etc
}

这是最好的答案,静态钩子看起来像是黑客。依赖注入是你的朋友。 - ericn

4

对于那些使用 Kotlin 并且使用 Rule 而不是创建一个 companion object 的人,可以使用 @get:Rule

所以,不要使用:

companion object {
 @ClassRule
 @JvmField
 val schedulers = RxImmediateSchedulerRule()
}

您可以简单地使用:
@get:Rule
val schedulers = RxImmediateSchedulerRule()

4
如果您仍然遇到问题,以上代码均无法帮助您, 此外,向您的app.gradle文件添加以下行也是一个不错的建议:
testOptions {
    animationsDisabled = true
    unitTests {
        includeAndroidResources = true
        returnDefaultValues = true
    }
}

这对我很有帮助。以上的解决方案都不适用于我,因为它们没有包含导入(一如既往),而作为一个新手Android开发者,我不知道它们所引用的框架。 - Mario Codes

2

我遇到了这个问题并且来到了这篇文章,但是我没有找到针对RX 1的任何解决方案。所以如果你在第一个版本上遇到同样的问题,这就是解决方案。

@BeforeClass
public static void setupClass() {
    RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() {
        @Override
        public Scheduler getMainThreadScheduler() {
            return Schedulers.trampoline();
        }
    });
}

2

补充starkej2的答案,对我很有效,直到我在测试Observable.timer()时遇到stackoverflowerror。没有相关帮助,但幸运的是,我使用以下调度程序定义使其工作,并通过所有其他测试。

new Scheduler() {
            @Override
            public Worker createWorker() {
                return new ExecutorScheduler.ExecutorWorker(new ScheduledThreadPoolExecutor(1) {
                    @Override
                    public void execute(@NonNull Runnable runnable) {
                        runnable.run();
                    }
                });
            }
        };

就像starkej2的回答一样,保持休息。希望这能帮助到有需要的人。


1
谢谢!实际上,我在发布我的答案后不久也遇到了同样的问题。我的解决方法和你的类似...我会更新答案,因为这些信息可能对人们有用。 - starkej2

1
对于RxJava 1,您可以像这样创建不同的调度程序:
 @Before
 public void setUp() throws Exception {
    // Override RxJava schedulers
    RxJavaHooks.setOnIOScheduler(new Func1<Scheduler, Scheduler>() {
        @Override
        public Scheduler call(Scheduler scheduler) {
            return Schedulers.immediate();
        }
    });

    RxJavaHooks.setOnComputationScheduler(new Func1<Scheduler, Scheduler>() {
        @Override
        public Scheduler call(Scheduler scheduler) {
            return Schedulers.immediate();
        }
    });

    RxJavaHooks.setOnNewThreadScheduler(new Func1<Scheduler, Scheduler>() {
        @Override
        public Scheduler call(Scheduler scheduler) {
            return Schedulers.immediate();
        }
    });

    // Override RxAndroid schedulers
    final RxAndroidPlugins rxAndroidPlugins = RxAndroidPlugins.getInstance();
    rxAndroidPlugins.registerSchedulersHook(new RxAndroidSchedulersHook() {
        @Override
        public Scheduler getMainThreadScheduler() {
            return Schedulers.immediate();
    }
});
} 

@After
public void tearDown() throws Exception {
RxJavaHooks.reset();
RxAndroidPlugins.getInstance().reset();
}

使用Retrofit和RxJava进行Android应用程序单元测试


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接