Mocking the UI for unit tests

Anup Ammanavar
Level Up Coding
Published in
4 min readApr 7, 2024

--

Photo by Kelly Sikkema on Unsplash

In this article, we explore the challenges of testing ViewModels and the problems associated with the testing methodologies adopted. It also offers insights for improvement with a more effective methodology.

Problem statement

Typically, during unit testing, we test the repositories, use cases, and other smaller components. Evaluating these isolated units poses no challenge as their scope is very limited. Better code coverage ensures better quality.

But, with ViewModels, ensuring quality by increasing the code coverage poses a challenge. This is primarily because of the wider scope of the ViewModels & the testing methodologies adopted.

One common pitfall observed is the testing the code rather than testing the ViewModel’s intended behaviour.

Proposition [Scenario-based testing]

The idea is to shift our testing approach by focusing on user scenarios, thereby creating more holistic tests.

The concept of scenario-based testing is typically prevalent in UI and integration testing, and the same strategy will be considered here. The primary challenge while adopting this in unit-test is the absence of a UI-layer. To address this, a Mock UI-layer is introduced to simulate the user actions.

While implementing this, the throughout process is also to

  • Improve the readability of test cases for broader audience, including QA, Product, and business stakeholders.
  • Ensuring the quality of test cases by establishing an architecture with clear layering

We will be creating a ProxyUI to mock the UI behaviour & simulate user actions. And test cases would comprise of the user actions (👇).

Attaching a reference of user scenario-based & readable test case.

ProxyUI — HLD

1. Understanding Unidirectional data flow

Events flow up from UI → ViewModel and State flows down, ViewModel → UI. So, generally, there are 2 actors

  • UI → event producer
  • ViewModel → event consumer
Control flow

2. Introducing Proxy UI

Since, we don’t have the UI, we will be mocking it (Curious again? Wait for the implementation).

ProxyUI

ProxyUI — Implementation

Let’s consider a login page where the user can enter username, password and click on the login button

1.Defining the UiEventConsumer

sealed class LoginUiEvents {
data class NameChanges(val name: String) : LoginUiEvents()
data class PasswordChanges(val password: String) : LoginUiEvents()
object OnLoginClicked : LoginUiEvents()
}

interface LoginUiEventsConsumer {
fun process(uiEvent: LoginUiEvents)
}

2. Implementation of the ProxyUI

ProxyUI takes the user events and passes them to the eventsConsumer(ViewModel)

interface LoginUI {
fun enterName(name: String)
fun enterPassword(password: String)
fun clickLogin()
}

class LoginProxyUI(
private val eventsConsumer: LoginUiEventsConsumer,
): LoginUI {
override fun enterName(name: String) {
performAction(LoginUiEvents.NameChanges(name))
}

override fun enterPassword(password: String) {
performAction(LoginUiEvents.PasswordChanges(password))
}
override fun clickLogin() {
performAction(LoginUiEvents.OnLoginClicked)
}
private fun performAction(uiEvent: LoginUiEvents) {
eventsConsumer.process(uiEvent)
}
}

3. Using the ProxyUI in our test cases

class LoginViewModelTest

lateinit var viewModel: LoginViewModel
private val ui: LoginUI by lazy { LoginProxyUI(viewModel) }
fun loginWithCorrectUsernameAndPassword() {
// 1.Given (Build the unit under test)
viewModel = LoginViewModel()
// 2.When (Define the user scenarios)
with(ui) {
enterName(CORRECT_USERNAME)
enterPassword(CORRECT_PASSWORD)
clickLogin()
}
// 3.Then (Verify the desired result)
assert("Check if the user is logged in")
}
fun loginWithoutPassword() {
viewModel = LoginViewModel()
with(ui) {
enterName("John")
clickLogin()
}
assert("Check if the error state is shown")
}
}

Each unit test comprises three integral phases:

  1. Build the unit that is being tested.
  2. Perform the user actions. Here, we define the user scenarios by using the ProxyUI
  3. Verify the result.

Emphasise on the below part where the user scenarios are defined using ProxyUI

with(ui) {
enterName(CORRECT_USERNAME)
enterPassword(CORRECT_PASSWORD)
clickLogin()
}

Closing points

As we strive to write human-readable code, leveraging tools like GitHub Co-pilot can significantly reduce development efforts from days to hours.

If you plan for Instrumentation tests, with clear layering, simply swap the ProxyUI with the actual UI and use tools like Espresso to simulate user actions.

Note that in test cases, we only reference the UI, enforcing scenario based tests without direct interaction with the ViewModel.

Quality matters, it’s time to embrace the joy of writing unit tests ❤️

Github link of the project

--

--