So, I am currently trying to test some time sensitive code in my Android app:
MainPresenter I use interval() and delay() to do a countdown and display text in the View accordingly. Both, by default, use Schedulers.computation().TestScheduler so that I am able to forward the time and check if the the correct methods are called on a mock of the View.RxJavaPlugins and RxJavaSchedulersHook to override the computation Scheduler to use my TestScheduler instead. I also keep a reference to the latter in my test class and use that in each unit test.See this code, which _almost_ works:
public class MainPresenterTest {
private TestScheduler testScheduler;
@Before
public void setupSchedulers() {
testScheduler = new TestScheduler();
RxJavaPlugins.getInstance().registerSchedulersHook(new RxJavaSchedulersHook() {
@Override
public Scheduler getComputationScheduler() {
return testScheduler;
}
@Override
public Scheduler getIOScheduler() {
return testScheduler;
}
@Override
public Scheduler getNewThreadScheduler() {
return testScheduler;
}
});
RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() {
@Override
public Scheduler getMainThreadScheduler() {
return Schedulers.immediate();
}
});
}
@Test
public void nothingHappensInTheFirstThreeSeconds() {
MainPresenter.View mockedMainPresenterView = mock(MainPresenter.View.class);
MainPresenter mainPresenter = new MainPresenter(mockedMainPresenterView);
mainPresenter.onResume();
verifyZeroInteractions(mockedMainPresenterView);
testScheduler.advanceTimeTo(2_999, TimeUnit.MILLISECONDS);
verifyZeroInteractions(mockedMainPresenterView);
}
@Test
public void theWholeSequenceCompletesAfterEightSeconds() {
MainPresenter.View mockedMainPresenterView = mock(MainPresenter.View.class);
MainPresenter mainPresenter = new MainPresenter(mockedMainPresenterView);
mainPresenter.onResume();
verifyZeroInteractions(mockedMainPresenterView);
testScheduler.advanceTimeTo(8, TimeUnit.SECONDS);
verify(mockedMainPresenterView).displayText("5");
verify(mockedMainPresenterView).displayText("4");
verify(mockedMainPresenterView).displayText("3");
verify(mockedMainPresenterView).displayText("2");
verify(mockedMainPresenterView).displayText("1");
verify(mockedMainPresenterView).displayText("Hello World!");
verifyNoMoreInteractions(mockedMainPresenterView);
}
Actually, it works fine as long as there is only one test case. If I add another one, I get the following exception when @Before is running for the second time:
java.lang.IllegalStateException: Another strategy was already registered: ...
I have now done some research and found several candidates for a solution:
RxJavaPlugins.getInstance().reset(); and a custom TestRule. See also this discussion about making reset() publicIf you were relying on the ability to change the main thread scheduler over time (such as for tests), return a delegating scheduler from the hook which allows changing the delegate instance at will.
I am assuming this method would also work for any other RxJava scheduler - if I knew how to do it.
So, which of these (or any other) methods is preferred for resetting/changing the TestScheduler that's used for overriding Schedulers.computation() in between tests? In particular, how would the third alternative work exactly? How would that "delegating scheduler" look or work?
Thanks for any advice!
You should be able now to reset the plugins.
Both RxJava and RxAndroid cache their schedulers on first use. Resetting
the plugins only lets you do things like alter the hook callbacks that are
use per-invocation.
On Thu, May 5, 2016 at 3:47 PM David Karnok [email protected]
wrote:
You should be able now to reset the plugins.
—
You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub
https://github.com/ReactiveX/RxJava/issues/3914#issuecomment-217257097
@DavidMihola I'd recommend to not depend on RxJava default schedulers directly and somehow pass them to the target code so you'll be able to use whatever schedulers you need for testing without problems.
RxJava plugin mechanism is a global state, don't forget to reset it back @After tests, otherwise you may affect other tests in the project.
@artem-zinnatullin Thanks for the suggestion!
I've done that before - pass the Scheduler in constructors and/or methods calls and then forward them to interval(), delay(), etc - but I don't really like the additional complexity it introduces to my code. I'd rather make the test setup more complex and keep the real code clean.
Was your recommendation only based on the testing problems I described or do you generally prefer passing Schedulers in to getting the defaults from the static methods in Schedulers?
Also, thanks for reminding me of @After - I'd probably have run into that sooner or later and wondered what was the problem...
OK, so RxJavaPlugins.getInstance().reset() does not help because - as Jake pointed out - the Schedulers are cached and Schedulers.computation() will always return the Scheduler used in the first test case, even though the @Before runs between each test.
So I tried to implement the suggested DelegatingScheduler:
public class DelegatingScheduler extends Scheduler {
private TestScheduler innerScheduler;
@Override
public Worker createWorker() {
return innerScheduler.createWorker();
}
@Override
public long now() {
return innerScheduler.now();
}
public TestScheduler getInnerScheduler() {
return innerScheduler;
}
public void setInnerScheduler(TestScheduler innerScheduler) {
this.innerScheduler = innerScheduler;
}
}
Does that look reasonable? It seems to work correctly if I use it like this:
public class MainPresenterTest {
private static DelegatingScheduler delegatingScheduler;
private TestScheduler testScheduler;
@BeforeClass
public static void createDelegatingScheduler() {
delegatingScheduler = new DelegatingScheduler();
RxJavaPlugins.getInstance().reset();
RxJavaPlugins.getInstance().registerSchedulersHook(new RxJavaSchedulersHook() {
@Override
public Scheduler getComputationScheduler() {
return delegatingScheduler;
}
@Override
public Scheduler getIOScheduler() {
return delegatingScheduler;
}
@Override
public Scheduler getNewThreadScheduler() {
return delegatingScheduler;
}
});
RxAndroidPlugins.getInstance().reset();
RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() {
@Override
public Scheduler getMainThreadScheduler() {
return Schedulers.immediate();
}
});
}
@Before
public void resetTestScheduler() {
testScheduler = new TestScheduler();
delegatingScheduler.setInnerScheduler(testScheduler);
}
@Test
public void nothingHappensInTheFirstThreeSeconds() {
MainPresenter.View mockedMainPresenterView = mock(MainPresenter.View.class);
MainPresenter mainPresenter = new MainPresenter();
mainPresenter.setView(mockedMainPresenterView);
mainPresenter.onResume();
verifyZeroInteractions(mockedMainPresenterView);
testScheduler.advanceTimeTo(2_999, TimeUnit.MILLISECONDS);
verifyZeroInteractions(mockedMainPresenterView);
}
@Test
public void afterFiveSecondsWeHaveSeenSomeUpdates() {
MainPresenter.View mockedMainPresenterView = mock(MainPresenter.View.class);
MainPresenter mainPresenter = new MainPresenter();
mainPresenter.setView(mockedMainPresenterView);
mainPresenter.onResume();
verifyZeroInteractions(mockedMainPresenterView);
testScheduler.advanceTimeTo(3, TimeUnit.SECONDS);
verify(mockedMainPresenterView).displayText("5");
verifyNoMoreInteractions(mockedMainPresenterView);
testScheduler.advanceTimeTo(4, TimeUnit.SECONDS);
verify(mockedMainPresenterView).displayText("4");
verifyNoMoreInteractions(mockedMainPresenterView);
testScheduler.advanceTimeTo(5, TimeUnit.SECONDS);
verify(mockedMainPresenterView).displayText("3");
verifyNoMoreInteractions(mockedMainPresenterView);
}
@Test
public void theWholeSequenceCompletesAfterEightSeconds() {
MainPresenter.View mockedMainPresenterView = mock(MainPresenter.View.class);
MainPresenter mainPresenter = new MainPresenter();
mainPresenter.setView(mockedMainPresenterView);
mainPresenter.onResume();
verifyZeroInteractions(mockedMainPresenterView);
testScheduler.advanceTimeTo(3, TimeUnit.SECONDS);
verify(mockedMainPresenterView).displayText("5");
verifyNoMoreInteractions(mockedMainPresenterView);
testScheduler.advanceTimeTo(4, TimeUnit.SECONDS);
verify(mockedMainPresenterView).displayText("4");
verifyNoMoreInteractions(mockedMainPresenterView);
testScheduler.advanceTimeTo(8, TimeUnit.SECONDS);
verify(mockedMainPresenterView).displayText("3");
verify(mockedMainPresenterView).displayText("2");
verify(mockedMainPresenterView).displayText("1");
verify(mockedMainPresenterView).displayText("Hello World!");
verifyNoMoreInteractions(mockedMainPresenterView);
}
}
Any comments, criticisms or confirmations very much appreciated!
Version 1.1.6 introduced the ability to reset the Schedulers. I assume that should help you with your original problem. If you have further input on the issue, don't hesitate to reopen this issue or post a new one.
Most helpful comment
Version 1.1.6 introduced the ability to reset the
Schedulers. I assume that should help you with your original problem. If you have further input on the issue, don't hesitate to reopen this issue or post a new one.