Blog Infos
Author
Published
Topics
, , , ,
Published

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.

Photo by Mourizal Zativa on Unsplash

 

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.

Example of user actions in test cases
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
HLD defining the UDF & the components

 

2. Introducing Proxy UI

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

Replacing actual UI with Proxy UI

 

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Intro to unit testing coroutines with Kotest & MockK

In this workshop, you’ll learn how to test coroutines effectively using the Kotest and MockK libraries, ensuring your app handles concurrent tasks efficiently and with confidence.
Watch Video

Intro to unit testing coroutines with Kotest & MockK

Jaroslaw Michalik
Kotlin GDE

Intro to unit testing coroutines with Kotest & MockK

Jaroslaw Michalik
Kotlin GDE

Intro to unit testing coroutines with Kotest & MockK

Jaroslaw Michali ...
Kotlin GDE

Jobs

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

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu