Complete example of testing MVP architecture with Kotlin and RxJava — Part 2

Tamás Kozmér
AndroidPub
Published in
6 min readOct 4, 2017

--

This is the second part of the article about testing every layer of an MVP app in Kotlin. In the first part we discussed the testing of the model layer and the interactor. If you missed it, you can check it out here.

In this part I will show you how to test the presenter by substituting the RxJava schedulers with test schedulers using the RxJavaPlugins and dependency injection. We will also see how to control the time of the schedulers in our tests.

Testing the UserListPresenter

The code of UserListPresenter is quite simple. It has only two public methods.

  • getUsers — Requests the users from the interactor, and updates the UI according to the results.
  • onScrollChanged — Handles the scroll change of the RecyclerView. If we reach a specific element in the list, we already start to fetch the next page in the background, and show a loading indicator only, when the user reaches the last item, and the loading is still not finished.

The problem with testing this class is that we have dependencies on RxJava schedulers. We have multiple options to fix this.

Overriding default RxJava schedulers with immediate scheduler

First we will see how to substitute the RxJava schedulers with a scheduler, that instantly runs commands with the help of RxJavaPlugins.

RxJavaPlugins is a utility class, what allows us to modify the default behavior of RxJava. We only need it to change the default schedulers, but it can be used to change several other things about how RxJava works.

First let’s write a simple test for UserListPresenter and see what happens.

If you have read the first part, there should be nothing new here. We create some mock objects using Mockito, call some methods on the UserListPresenter, and then verify the expected behavior.

But if we try to run this test, we face the following error:

java.lang.ExceptionInInitializerError
...
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)

This happens because the AndroidSchecdulers.mainThread() has dependencies from the Android framework, and we are creating a local unit test.

That’s where we can use the RxJavaPlugins and RxAndroidPlugins utility classes to override the default schedulers.

First we create an immediateScheduler field in our test class. We must extend the Scheduler class from RxJava and override the createWorker method to run the actions immediately. Then in the setUp method we call the static methods of the RxJavaPlugins and RxAndroidPlugins to override the schedulers. The below snippet shows the implementation of this. We also need to reset the schedulers in the tearDown method.

Now our test will pass. You can see that we overrode only two schedulers, but there are more in RxJava. Here we only use these two in the UserListPresenter, so it’s unnecessary to override the rest of them.

That’s great, but what if we have 10 presenters, and we need to do this in all of our tests? Fortunately we don’t have to do this. We can create a test rule, where we override the schedulers, and apply it in every test, where we need it.

In the test rule we override every scheduler, so if we use other schedulers, we can use the same test rule everywhere. If we never use a specific scheduler in our app, we can exclude it from the test rule as well.

To use our newly created test rule, we need to add the following code to our test class.

@Rule @JvmField
val immediateSchedulerRule = ImmediateSchedulerRule()

We need to add the @JvmField annotation, because the @Rule annotation works only on fields and getter methods, but the immediateSchedulerRule is a property in Kotlin.

That’s it, now we can test our presenters using the immediate scheduler. The changes can be found in this commit (it also contains some test cases, what aren’t shown here):

https://github.com/kozmi55/Kotlin-MVP-Testing/commit/c885758f47f58a5a818d8c9ff070190cc2a26e26

Using the TestScheduler to control the time

The immediate scheduler is enough for most cases. But sometimes we need to control the time to test some functionality. Look at the onScrollChanged method in the UserListPresenter. How would you test it? The loading field will always be false, because the getUsers executes immediately. We can set the field public, but that would be bad practice to expose a field just because of testing.

RxJava has the TestScheduler class for these cases. This is a special scheduler, that allows us to manually advance a virtual time. Let’s see this in a simple example:

With the delay method we can create a Single, what doesn’t complete immediately. The third parameter of the method is a Scheduler. If we pass a TestScheduler instance, those 2 seconds will be virtual seconds. Now we can use the methods of the TestScheduler to manipulate this virtual time. This can be seen in row 18 of the example.

In our example, we have a Single, that takes 2 seconds to complete, and we advance the time only by 1 second. So when we scroll down, mockGetUsers.execute won’t get called one more time, because the first call is still loading, therefore we should verify, that the method will be called only once.

TestScheduler also have a advanceTimeTo method, what moves the time to a particular moment.

Injecting the TestScheduler

We could use the same way to replace the default schedulers with TestScheduler, what we used before, but for some reason it produced a strange error to me. When I ran the whole class of tests at once, only the first test passed, the rest of them always failed, because the actions were not triggered (I used TestScheduler.triggerAction method for simpler tests, where I didn’t need to control time). To solve this we need to use the advanceTimeBy method instead of triggerAction, even if we don’t need to control the time.

Although this solution works, this made me realize, that there is a much cleaner way to replace the schedulers, and it is dependency injection.

To do this first we create a SchedulerProvider interface, and two implementations of it.

  • AppSchedulerProvider — This will provide us the real schedulers. We will inject this class to all of our presenters, and this will provide the schedulers for our Rx subscriptions.
  • TestSchedulerProvide — This class will provide us a TestScheduler instead of the real schedulers. When we instantiate our presenter in the test, we will use this as it’s constructor parameter.
To make it simple, I added these 3 classes to the same gist, but in the project they are in a separate file.

Now we need to add the SchedulerProvider as a constructor parameter in the UserListPresenter and change the following lines

.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())

to this:

.subscribeOn(schedulerProvider.ioScheduler())
.observeOn(schedulerProvider.uiScheduler())

We also need to add a provider method in our ApplicationModule, to provide the SchedulerProvider dependency.

@Provides
@Singleton
fun provideSchedulerProvider() : SchedulerProvider = AppSchedulerProvider()

Now we can use the TestSchedulerProvider in our tests the following way:

If we want to use the TestScheduler in the test, we get the property of the provider: testSchedulerProvider.testScheduler

That’s it. You can find more test cases, where we manipulate the time in the repository. I created some private utility methods to extract the common parts of these tests, and make the code cleaner. You can find it in this commit:

https://github.com/kozmi55/Kotlin-MVP-Testing/commit/eed5f3a938ac0fdc3a75ccfaede902c54810f56a

Thanks for reading the second part of the series. We covered the topic of testing the presenters when using RxJava and learned different techniques for dealing with the RxJava schedulers in tests.

In the last part we will see how to make UI tests with fake data using Espresso and how to deal with some Kotlin specific problems in Espresso tests.

If you liked it, hit the clap button or share it with fellow Android developers.

If you have any questions or suggestions leave a comment below.

--

--