/ android

Testing Reactive Presenters: Destructure Your ViewModel Lists!

Back in the early days of Kotlin adoption on Android, many people started writing their unit tests using Kotlin. Even though the primary goal of this approach was to have a possibility to enjoy Kotlin without letting it slip into the actual production code, it turned out that Kotlin is a great language for writing descriptive unit tests, due to its conciseness and elegancy. I still regularly discover new ways of applying Kotlin's numerous features to improve the code I write, and I'd like to share a neat little testing trick that I've come across just recently.

Imagine the following setup:

data class PageViewModel(
  val title: String? = null,
  val text: String? = null,
  val loading: Boolean = false
)

class PagePresenter(
  private val args: PageScreenArgs,
  private val pageService: PageService,
  private val backgroundScheduler: Scheduler
) {

  fun subscribe(): Observable<PageViewModel> = buildViewModel()

  private fun buildViewModel(): Observable<PageViewModel> {
    return pageService.loadPage(args.pageId)
        .map { page -> PageViewModel(title = page.title, text = page.text) }
        .startWith(PageViewModel(loading = true))
        .subscribeOn(backgroundScheduler)
  }
}

First, we've got a ViewModel called PageViewModel, which is based on an entity called Page. We use two flavors of PageViewModel: PageViewModel(loading = true), which will tell our View to display a loading spinner, and PageViewModel(title = page.title, text = page.text), which carries the actual data that the View will render.

Now let's decrypt the RxJava magic that happens in our Presenter: first, we load a Page using the PageService and map it into a PageViewModel. The startsWith operator allows us to specify an element that will appear as the first element in the stream and will be emitted as soon as the View subscribes to the Presenter. This behavior matches our use-case: we want to show the spinner as soon as the View is displayed, and replace it with the actual data as soon as it's available. Awesome!

Now, let's unit-test this logic! Here's the test case:

@Test fun `builds correct view model`() {
  val presenter = createPresenter(pageId = "page-0")
  `when`(pageService.loadPage(id = "page-0"))
      .thenReturn(Observable.just(Page(title = "Hello world!", text = "Lorem ipsum")))

  val values = presenter.subscribe().test().values()
  assertThat(values).hasSize(2)
  val loading = values[0]
  assertThat(loading.loading).isTrue()
  val page = values[1]
  assertThat(page.title).isEqualTo("Hello world!")
  assertThat(page.text).isEqualTo("Lorem ipsum")
}

test() is an extremely useful method that returns a TestObserver, a utility class that makes testing Rx streams easy. The method we're using is values(), which simply gives us all events as a List<T>. We then verify that the list has the correct size and proceed with checking conditions on each of the ViewModels we expect to get. There's nothing wrong with this code, but there's a nice Kotlin feature that can make it more elegant - list destructuring! Check this out:

@Test fun `builds correct view model`() {
  val presenter = createPresenter(pageId = "page-0")
  `when`(pageService.loadPage(id = "page-0"))
      .thenReturn(Observable.just(Page(title = "Hello world!", text = "Lorem ipsum")))

  val (loading, page) = presenter.subscribe().test().values()
  assertThat(loading.loading).isTrue()
  assertThat(page.title).isEqualTo("Hello world!")
  assertThat(page.text).isEqualTo("Lorem ipsum")
}

Here the first and second elements in the list will automatically get assigned to loading and page respectively, so we won't have to query them explicitly. A word of caution: list destructuring doesn't perform the size check for you. It will still work if you're declaring less variables then the number of elements in the list:

val (loading) = presenter.subscribe().test().values() // Works!

However, if the number of variables is bigger then the number of elements in the list - you'll get hit with an ArrayIndexOutOfBoundsException:

val (loading, page, third) = presenter.subscribe().test().values() // Mmm nope

TestObserver to the rescue:

@Test fun `builds correct view model`() {
  val presenter = createPresenter(pageId = "page-0")
  `when`(pageService.loadPage(id = "page-0"))
      .thenReturn(Observable.just(Page(title = "Hello world!", text = "Lorem ipsum")))

  val testObserver = presenter.subscribe().test()
  testObserver.assertValueCount(2)
  
  val (loading, page) = testObserver.values()
  assertThat(loading.loading).isTrue()
  assertThat(page.title).isEqualTo("Hello world!")
  assertThat(page.text).isEqualTo("Lorem ipsum")
}

This version properly checks the preconditions and destructures the list to perform asserts on individual elements.

Enjoy!

Thanks to Aashni and Alec, my colleagues at Square Cash, for reviewing this article.

Testing Reactive Presenters: Destructure Your ViewModel Lists!
Share this

Subscribe to Egor's Blog