Rxjava: How to replace TestScheduler in RxJavaSchedulersHook?

Created on 5 May 2016  Â·  6Comments  Â·  Source: ReactiveX/RxJava

So, I am currently trying to test some time sensitive code in my Android app:

  • Inside my MainPresenter I use interval() and delay() to do a countdown and display text in the View accordingly. Both, by default, use Schedulers.computation().
  • For testing, I'd like to use a 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.
  • Therefore I am using 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:

If 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!

Question

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.

All 6 comments

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

nltran picture nltran  Â·  4Comments

dsvoronin picture dsvoronin  Â·  4Comments

paulblessing picture paulblessing  Â·  3Comments

archenroot picture archenroot  Â·  3Comments

theblang picture theblang  Â·  3Comments