Mobile Development 12 min read

Automated Unit Testing Framework for Android ViewModel (Part 4)

Part 4 of Airbnb’s Android testing series presents a dedicated framework that lets developers write concise, single‑function unit tests for ViewModel classes by specifying initial state, parameters, and expected state or dependency calls through a Kotlin DSL, integrating with JUnit, Robolectric, Mockito, and offering scaffolding and IntelliJ plugins for streamlined, systematic ViewModel logic verification.

Airbnb Technology Team
Airbnb Technology Team
Airbnb Technology Team
Automated Unit Testing Framework for Android ViewModel (Part 4)

This article is the fourth part of Airbnb's Android automated testing series. It introduces a dedicated framework for automating unit tests of ViewModel classes.

While previous articles showed how to test ViewModel behavior by recording state changes, that approach cannot cover all edge cases. The framework aims to provide deeper, more systematic testing of ViewModel logic.

Core principles of the framework :

Each ViewModel function should be independently testable; the design must not rely on interactions between functions.

The behavior of a function is fully determined by the ViewModel's current state and the parameters passed to the function.

The output of a function is either a new state for the ViewModel or a call to a dependency.

Based on these principles, the framework defines a unit‑test workflow where:

Each test invokes a single ViewModel function.

The test input consists of the ViewModel's initial state plus the parameters for the function under test.

The test output asserts the expected state change and/or verifies expected dependency calls (using Mockito).

Example: a simple TextViewModel

data class TextState(val text: String? = null) : MvRxState
class TextViewModel(state: TextState) : MvRxViewModel
(state) {
fun setText(text: String) {
setState {
copy(text = text)
}
}
}

Test for setText :

@Test
fun setText() = TextViewModel::setText {
withParams("hello") expectState {
copy(text = "hello")
}
}

The test declares the function reference, the parameters, and the expected resulting state. The expectState block receives the initial state, returns the expected output state, and the framework fails the test if the actual state does not match.

The framework integrates with standard JUnit and Robolectric setups. Each test class implements an interface that provides a buildViewModel() method to create a fresh ViewModel instance for every test.

class TextViewModelTest : ViewModelTest
{
override fun buildViewModel() = TextViewModel(TextState())
@Test
fun setText() = TextViewModel::setText {
withParams("hello") expectState {
copy(text = "hello")
}
}
}

Extending the model with nested state (e.g., a bold flag) demonstrates the DSL for handling complex state structures.

data class TextOptions(val bold: Boolean = false)
data class TextState(val text: String? = null, val options: TextOptions = TextOptions()) : MvRxState
class TextViewModel(state: TextState) : MvRxViewModel
(state) {
fun setBold(bold: Boolean) {
setState {
copy(options = options.copy(bold = bold))
}
}
}

Test for setBold :

@Test
fun setBold() = TextViewModel::setBold {
initialState { setFalse { ::options { ::bold } } }
withParams(true) expectState {
setTrue { ::options { ::bold } }
}
}

The DSL also supports pluggable extensions, allowing tests to assert network requests. The following example adds a loadText function that performs a network request and updates the state.

fun loadText(textId: Long) {
buildRequest
(
path = "text/endpoint",
params = { kv("id", textId) }
).execute {
copy(text = it)
}
}

Test for loadText :

@Test
fun loadText() = TextViewModel::loadText {
withParams(1)
expectRequests {
GET("text/endpoint?id=1") shouldReturn "server result"
} expectState {
copy(text = "server result")
}
}

Additional utilities include sets for single‑property tests and setsMapped for mapping multiple inputs to expected outputs, e.g. testing a function that squares a number.

@Test
fun squared() = TestViewModel::squareNumber {
setsMapped(2 to 4, 5 to 25) { ::result }
}

Initialization tests verify that a ViewModel performs network calls during construction:

@Test
fun initialization() = testInitialization(
expectRequests = { GET("text/endpoint") },
expectState = { copy(text = Loading()) }
)

The article also mentions tooling that automatically generates test scaffolding for new modules (Robolectric runner, Dagger test modules, Mockito plugins) and an IntelliJ plugin that creates a test file whenever a new MvRx ViewModel is generated.

In summary, the framework aims to make ViewModel logic testing easier while providing a flexible API that can cover all use cases. The next article (part 5) will discuss the overall automation infrastructure supporting integration and screenshot tests.

DSLViewModelAndroidKotlinunit testingMockitoRobolectric
Airbnb Technology Team
Written by

Airbnb Technology Team

Official account of the Airbnb Technology Team, sharing Airbnb's tech innovations and real-world implementations, building a world where home is everywhere through technology.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.