Chapters

Hide chapters

Reactive Programming with Kotlin

Second Edition · Android 10 · Kotlin 1.3 · Android Studio 4.0

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section II: Operators & Best Practices

Section 2: 7 chapters
Show chapters Hide chapters

15. Testing RxJava Code
Written by Alex Sullivan

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

First and foremost — you’re a hero for not skipping this chapter! Testing your code is at the heart of writing good software — RxJava comes with lots of nifty tricks for testing everything under the sun. In this chapter, you’ll use JUnit to write unit tests to test a few operators and this chapter’s app.

Getting started

You’re going to be working on an app named HexColor for this chapter. HexColor is a nifty app that lets you input a hex color string. The app then shows you that color and (if it’s within a set of known hex colors) tells you what the name of the color is. Open the starter project and run the app. You should see an app that looks like this:

Enter a full hex string to see the app in action. It wouldn’t be a color-based app if there wasn’t some product placement, so try to enter the Ray Wenderlich green color: #006636

You should see the following screen:

In the top-left, you can see the color broken up by RGB values. On the right, you can see the name of the color.

Fancy, right?

Now that you’re thoroughly impressed, take a look at the ColorViewModel class to see what’s going on inside. Most of the logic for the app is actually contained in the init block:

// Send the hex string to the activity
hexStringSubject
  .subscribeOn(backgroundScheduler)
  .observeOn(mainScheduler)
  .subscribe(hexStringLiveData::postValue)
  .addTo(disposables)

// Send the actual color object to the activity
hexStringSubject
  .subscribeOn(backgroundScheduler)
  .observeOn(mainScheduler)
  .map { if (it.length < 7) "#FFFFFF" else it }
  .map { colorCoordinator.parseColor(it) }
  .subscribe(backgroundColorLiveData::postValue)
  .addTo(disposables)

// Send over the color name "--" if the hex string is less than
// seven chars
hexStringSubject
  .subscribeOn(backgroundScheduler)
  .observeOn(mainScheduler)
  .filter { it.length < 7 }
  .map { "--" }
  .subscribe(colorNameLiveData::postValue)
  .addTo(disposables)

// If our color name enum contains the given hex string, send
// that color name over.
hexStringSubject
  .subscribeOn(backgroundScheduler)
  .observeOn(mainScheduler)
  .filter {
    hexString -> ColorName.values().map { it.hex }
                   .contains(hexString)
  }
  .map { hexString -> ColorName.values().first {
     it.hex == hexString }
  }
  .map { it.toString() }
  .subscribe(colorNameLiveData::postValue)
  .addTo(disposables)

// Send the RGB values of the color to the activity.
hexStringSubject
  .subscribeOn(backgroundScheduler)
  .observeOn(mainScheduler)
  .map {
    if (it.length == 7) {
      colorCoordinator.parseRgbColor(it)
      } else {
        RGBColor(255, 255, 255)
      }
    }
    .map { "${it.red},${it.green},${it.blue}" }
    .subscribe(rgbStringLiveData::postValue)
    .addTo(disposables)

The hexStringSubject property is a BehaviorSubject, which receives hex string digits from the user as they come in via the digitClicked method. At any given moment, hexStringSubject has the whole hex string that the user has entered.

Each block in the above code subscribes to the hexStringSubject behavior subject, interprets the current string, and sends some information to the several live data objects contained in ColorViewModel.

The app is complete in its functionality — it just needs a few tests to make it perfect!

Weirdly enough, whoever wrote this app actually created two test classes with some plumbing already set up. How convenient!

Before you start writing tests for ColorViewModel, you need some background on testing in RxJava. To do that, you’ll start by writing a few sample RxJava tests in the OperatorTest class.

Introduction to TestObserver

Have you ever tried to test asynchronous code? If you have, you probably know that it’s no cake walk. It can be (very) difficult to both test all aspects of your asynchronous code and keep your unit tests running quickly. RxJava provides an extremely convenient set of test utilities to make testing Observables easier — the first of which is the TestObserver class.

@Test
fun `test concat`() {
  val observableA = Observable.just(1)
  val observableB = Observable.just(2)
  val observableC = observableA.concatWith(observableB)
}
observableC.test()
  .assertResult(1, 2)
  .assertComplete()

.assertResult(1)

Using a TestScheduler

In addition to TestObserver, the RxJava library exposes a special scheduler that you can use to control when your Observables emit items. That scheduler is called TestScheduler.

@Test
fun `test amb`() {
  // 1
  val observableA = Observable.interval(1, TimeUnit.SECONDS)
    .take(3)
    .map { 5 * it }
  val observableB = Observable
    .interval(500, TimeUnit.MILLISECONDS)
    .take(3)
    .map { 10 * it }

  // 2
  val ambObservable = observableA.ambWith(observableB)

  // 3
  val testObserver = ambObservable.test()

  testObserver.assertValueCount(3)
  testObserver.assertResult(0L, 10L, 20L)
  testObserver.assertComplete()
}
java.lang.AssertionError: Value counts differ; Expected: 3, Actual: 0 (latch = 1, values = 0, errors = 0, completions = 0)
val scheduler = TestScheduler()
val observableA = Observable.interval(1, TimeUnit.SECONDS, scheduler)
  .take(3)
  .map { 5 * it }
val observableB = Observable
  .interval(500, TimeUnit.MILLISECONDS, scheduler)
  .take(3)
  .map { 10 * it }
scheduler.advanceTimeBy(500, TimeUnit.MILLISECONDS)
testObserver.assertValueCount(1)
scheduler.advanceTimeBy(1000, TimeUnit.MILLISECONDS)

testObserver.assertValueCount(3)
testObserver.assertResult(0L, 10L, 20L)
testObserver.assertComplete()

Injecting schedulers

There will be many times in which you’re attempting to unit test classes that don’t directly expose an Observable. For example: Most of the ViewModel classes that you’ll see in this book don’t expose an Observable. Instead, they subscribe to those Observables internally and expose LiveData objects that work better with the Android lifecycle.

class Timer() {
  var elapsedTime: Int = 0

  init {
    val intervalObservable = Observable
      .interval(1, TimeUnit.SECONDS)
      .subscribeOn(Schedulers.io())
      .observeOn(AndroidSchedulers.mainThread())

    intervalObservable  
      .subscribe {
        elapsedTime++
      }
  }
}
class Timer(backgroundScheduler: Scheduler,
  mainThreadScheduler: Scheduler, timerScheduler: Scheduler) {
  var elapsedTime: Int = 0

  init {
    Observable.interval(1, TimeUnit.SECONDS, timerScheduler)
      .subscribeOn(backgroundScheduler)
      .observeOn(mainThreadScheduler)
      .subscribe {
        elapsedTime++
      }
  }
}

Using Trampoline schedulers

Now that you’re injecting schedulers, there’s another scheduler that can be very helpful when running unit tests.

@Test
fun `using trampoline schedulers`() {
  val observableA = Observable.just(1)
    .subscribeOn(TrampolineScheduler.instance())

  val observableB = Observable.just(1)
    .subscribeOn(Schedulers.io())
}
observableA.test().assertResult(1)
observableB.test().assertEmpty()

Using subjects with mocked data

One thing that can be very helpful is mocking data — that is, replacing one real piece of the puzzle with a different one that appears the same but which you have direct control over. This allows you to check that the other pieces of the puzzle work the way you expect them to when you feed them specified data.

// 1
class Photo

// 2
interface PhotoProvider {
  fun photoObservable(): Observable<Photo>
}

// 3
class PhotoViewModel(provider: PhotoProvider) {

  var disableButton = false
  private var photoList = arrayListOf<Photo>()

  init {
    // 4
    provider.photoObservable()
      .subscribe {
        photoList.add(it)
        if (photoList.size >= 5) {
          disableButton = true
        }
      }
  }
}
class PhotosTest {
  @Test
  fun `button disabled after 5 photos`() {
    val photoProviderMock = object: PhotoProvider {
      override fun photoObservable(): Observable<Photo> {
        TODO("Return some data")
      }
    }

    val viewModel = PhotoViewModel(photoProviderMock)

    Assert.assertFalse(viewModel.disableButton)
  }
}
val subject = PublishSubject.create<Photo>()
val photoProviderMock = object: PhotoProvider {
  override fun photoObservable(): Observable<Photo> {
    return subject
  }
}
subject.onNext(Photo())
Assert.assertFalse(viewModel.disableButton)
subject.onNext(Photo())
subject.onNext(Photo())
subject.onNext(Photo())
Assert.assertFalse(viewModel.disableButton)
subject.onNext(Photo())
Assert.assertTrue(viewModel.disableButton)

Testing ColorViewModel

Now that you’re an expert in testing, it’s time to add some real unit tests to the ViewModelTest class.

@Test
fun `color is red when hex string is FF0000`() {
}
// If our color name enum contains the given hex string, send that color name over.
hexStringSubject
  .subscribeOn(backgroundScheduler)
  .observeOn(mainScheduler)
  .filter { hexString ->
    ColorName.values().map { it.hex }.contains(hexString)
  }
  .map { hexString ->
    ColorName.values().first { it.hex == hexString }
  }
  .map { it.toString() }
  .subscribe(colorNameLiveData::postValue)
  .addTo(disposables)
val trampolineScheduler = TrampolineScheduler.instance()
val viewModel = ColorViewModel(trampolineScheduler,
  trampolineScheduler, colorCoordinator)
viewModel.digitClicked("F")
viewModel.digitClicked("F")
viewModel.digitClicked("0")
viewModel.digitClicked("0")
viewModel.digitClicked("0")
viewModel.digitClicked("0")

Assert.assertEquals(ColorName.RED.toString(),
  viewModel.colorNameLiveData.value)
@Test
fun `color is red when hex string is FF0000 using test scheduler`() {
  val testScheduler = TestScheduler()
  val viewModel = ColorViewModel(testScheduler,
    testScheduler, colorCoordinator)

  viewModel.digitClicked("F")
  viewModel.digitClicked("F")
  viewModel.digitClicked("0")
  viewModel.digitClicked("0")
  viewModel.digitClicked("0")
  viewModel.digitClicked("0")

  Assert.assertEquals(null, viewModel.colorNameLiveData.value)
  Assert.assertEquals(ColorName.RED.toString(),
    viewModel.colorNameLiveData.value)
}
java.lang.AssertionError:
Expected :RED
Actual   :null
testScheduler.triggerActions()
@Test
fun `hex subject is reset after clear is clicked`() {
}
hexStringSubject
  .subscribeOn(backgroundScheduler)
  .observeOn(mainScheduler)
  .subscribe(hexStringLiveData::postValue)
  .addTo(disposables)
@Test
fun `hex subject is reset after clear is clicked`() {
  val trampolineScheduler = TrampolineScheduler.instance()
  val viewModel = ColorViewModel(trampolineScheduler,
    trampolineScheduler, colorCoordinator)

  viewModel.digitClicked("F")
  viewModel.digitClicked("F")
  viewModel.digitClicked("0")
  viewModel.digitClicked("0")
  viewModel.digitClicked("0")
  viewModel.digitClicked("0")
}
Assert.assertEquals("#FF0000", viewModel.hexStringSubject.value)
viewModel.clearClicked()
Assert.assertEquals("#", viewModel.hexStringSubject.value)

Key points

Where to go from here?

Testing is an important piece to writing great apps. Hopefully after reading this chapter, you’ve picked up some tricks to use the next time you need to test some reactive code. Happy testing!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now