If you’re feeling lazy, there’s a TLDR at the bottom.
It’s a situation we’re all familiar with. We write one feature, it’s working finely. We write another feature, the other one breaks. Often times, we don’t even know it until our users start complaining about that one annoying bug they found in your app. Even more so if you’ve got a big application on your hands.
This is why we write tests. It’s known to be good practice in the Android and even the whole Software Development community as a whole to write tests for new features and code. Unfortunately, too many ignorant fools think it’s better to roll out features quicker than to write tests for every feature they’ve just created. I hear excuses such as “We’re in a rush to roll out new features” or “We’re already past our release deadline!”. Well Jimmy, you wouldn’t be stressing about your deadline if you didn’t have to spend 2 weeks on that one bug, would you?
Unit Testing on a surface level is pretty damn straightforward. You infer some expected values, run your functions and assert that your expected and actual values match. It gets slightly complicated when you have to introduce class dependencies but it’s still fine. Just create some mocks and you’re good to go.
Then we introduce observables and API calls into the scene… well crap. Things aren’t so straightforward anymore, are they Jimmy? Suddenly you’re worrying about asynchronous functions, huge chains of sequences and back-end code you can’t even touch. Your schedulers are causing NullPointerExceptions while you test them and writing a test now almost seems impossible. Jimmy you”re in luck, because I’m going to show you some simple tools that’ll make testing RxJava + Retrofit code doable.
Prerequisites
This is by no means an introduction to testing. This tutorial requires you to have previous knowledge with JUnit 4+, Mockito, and MVP Architectural Design.
Our Observable Function
disposable.add(mProductsRepository!!.getExampleShirts() .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribeBy( onSuccess = { products -> this.products = products getMvpView()?.displayProducts(products) }, onError = { throwable -> handleError(throwable) } ))
We’ll start with something simple. This Observable call simply makes an API call through our data repository (which leads almost directly to a simple GET Retrofit call). We do some thread switching and when we get our result, we tell the View to display it.
The Test we’re going to write
@Test fun whenGetExampleShirts_thenShirtsAreRetrieved() { }
We’re simply going to test that when we make the API call, our shirts are indeed retrieved.
First thing to note: Asynchronous code doesn’t run in the JVM. That means our regular subscriptions won’t run.
For Singles, use blockingGet()
(I’m talking about the Observable type, not you)
We can make use of blockingGet() to make our Single’s turn into synchronous functions that simply return their type value.
@Test fun `When get example shirts then shirts are retrieved`() { val exampleShirts = productsModel .observeExampleShirts() .blockingGet() assert(exampleShirts.isNotEmpty()) }
So this is our test. Simple and easy. (A nice trick with Kotlin as you can see is you can name your test functions more freely by encapsulating them in backticks (`). This works exclusively for unit tests and makes your list of tests so much easier to read through when you run them).
For standard Observables and Flowables, use TestSubscriber
We could indeed just use blockingSubscribe(), but there’s a few reasons why you shouldn’t:
- This blocks the current thread until the observable completes, extending the time of your tests immensely
- In the case of an Observable that doesn’t terminate, your test could be running forever
- There’s a much better way to test Observables
So what’s this better way you may ask? Introducing the TestSubscriber
@Test fun `When get example shirts then shirts are retrieved`() { val testSubscriber = TestSubscriber<List<Product>>() val exampleShirtsSubscription = productsModel .observeExampleShirts() .subscribe(testSubscriber) testSubscriber.assertNoErrors() testSubscriber.assertSubscribed() testSubscriber.assertComplete() }
Not only is it a much preferred method of testing observable streams to blockingSubscribe(), but you get a few handy extra assertions too which very much relate to the behavior of the observable.
@RunWith(MockitoJUnitRunner::class) class ProductsUnitTest { @Mock lateinit var mockProductsRepository: ProductsRepository lateinit var productsModel: ProductsModel @Before fun setUp() { MockitoAnnotations.initMocks(this) mModel = ProductsModel(mockProductsRepository) } @Test fun `When get example shirts then shirts are retrieved`() { // Mock the API Call returning a single item `when`(mockProductsRepository.getExampleShirts()).thenReturn( Single.just(listOf(Util.createSampleProduct())) ) val exampleShirts = mModel .observeExampleShirts() .blockingGet() assert(exampleShirts.isNotEmpty()) } }
So going back to our Single test for simplicity reasons, this is our test with all its mocking. Dead simple, not the most useful test but you get the idea. Mock the API, retrieve our list of products, and check that the list isn’t empty. Let’s see what happens when we run it.
java.lang.ExceptionInInitializerError at com.ericdecanini.sendclothing.mvp.presenter.ProductsModel.observeExampleShirts(ProductsModel.kt:52) Caused by: java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. at android.os.Looper.getMainLooper(Looper.java) at io.reactivex.android.schedulers.AndroidSchedulers$MainHolder.<clinit>(AndroidSchedulers.java:29)
Ok so that didn’t go well at all. We get an error where we call observeOn(AndroidSchedulers.mainThread()). The reason behind this is the JVM doesn’t like switching threads. Therefore, you have to fix it with this code.
@Before fun setUp() { MockitoAnnotations.initMocks(this) mModel = ProductsModel(mockProductsRepository) RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() } RxJavaPlugins.setComputationSchedulerHandler { Schedulers.trampoline() } RxJavaPlugins.setNewThreadSchedulerHandler { Schedulers.trampoline() } RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() } }
Schedulers.trampoline() is like an ‘immediate execution’ thread, and really the only one you want to be using for testing. It queues work on the current working thread. Fun fact, it used to be called Schedulers.immediate() in RxJava 1 (but was revised due to intense misuse). Therefore, we’ll use it to ‘become’ all our threads during our test. Now let’s see what happens when we run our test.
Process finished with exit code 0
Nice! Our test passes successfully. If we have to add these 4 lines of code to every setup function of every test, it does get a bit tedious. As always, there’s a solution to that.
class RxTrampolineSchedulerRule : TestRule { override fun apply(base: Statement, description: Description): Statement { return object : Statement() { @Throws(Throwable::class) override fun evaluate() { RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() } RxJavaPlugins.setComputationSchedulerHandler { Schedulers.trampoline() } RxJavaPlugins.setNewThreadSchedulerHandler { Schedulers.trampoline() } RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() } try { base.evaluate() } finally { RxJavaPlugins.reset() RxAndroidPlugins.reset() } } } } }
Then we get add it to our test class like so, and delete those 4 lines of code we had in our setUp() function.
@RunWith(MockitoJUnitRunner::class) class ProductsUnitTest { @Rule @JvmField var testSchedulerRule = RxTrampolineSchedulerRule() ... }
Now if we run our tests, we should get the exact same result.
Conclusion and TLDR
So what should you take from this?
1. Mock your Retrofit API Requests
2. Turn your Singles synchronous with blockingGet() or test your observable streams with TestSubscriber
3. Set all your Rx Threads to Schedulers.trampoline() on your @Before function.
4. Make that 3rd step more reusable by creating a Rule class to set all your Rx Threads.
BONUS: Backtick your test names!
Happy Coding ༼ つ ◕_◕ ༽つ