Logo

dev-resources.site

for different kinds of informations.

Getting Started with Android Testing: Building Reliable Apps with Confidence (Part 2/3)

Published at
1/6/2025
Categories
android
testing
kotlin
compose
Author
sridharsubramani
Categories
4 categories in total
android
open
testing
open
kotlin
open
compose
open
Author
16 person written this
sridharsubramani
open
Getting Started with Android Testing: Building Reliable Apps with Confidence (Part 2/3)

Learn the Fundamentals of Android Testing, One Step at a Time (Part 2/3)

Previous article


Target Audience for This Blog

This blog covers the basics of testing in Android, providing insights into setup, dependencies, and an introduction to different types of tests. It is designed to help beginners understand the fundamentals of Android testing and how various tests are implemented.


UI Testing

UI testing usually refers testing the user interface by simulating user action and verify the behavior of UI elements.

Famous UI Testing Frameworks

Framework Description
Espresso Android UI test framework to perform UI interaction and state assertion. (White box testing)
UI Automator To perform cross-app functional UI testing across system and installed apps. (Both Black box & white box testing)
Compose UI test Junit To provide Junit rules invoke composable function in Junit. also provides APIs to perform UI interaction and state assertion.

Compose UI + Interaction Unit Test

The Compose UI test framework allows you to verify that the behavior of your Compose code works as expected. It provides
a set of testing APIs that help you find UI elements, check their attributes, and perform user actions. Using these APIs, you can mount composable content and assert expected behaviors.

The androidx.compose.ui.test.junit4 module includes a ComposeTestRule and an implementation for Android called AndroidComposeTestRule. Through this rule you can set Compose content or access the activity. You construct the rules using factory functions, either createComposeRule or, if you need access to an activity, createAndroidComposeRule.

For Compose UI Unit Tests, you can use the RobolectricTestRunner, a JUnit Test Runner that runs test code directly on the JVM. This eliminates the need for a physical or virtual Android device, significantly speeding up test execution, ensuring consistent results, and simplifying the testing process.

However, some classes and methods from android.jar require additional configuration to function correctly. For example, accessing Android resources or using methods like Log might need adjustments to return default or mocked values. Please refer to the setup section below for the necessary configuration.


Example

In this test, we are verifying the behavior of the Login composable screen by ensuring that the login button is
enabled only when the inputs provided by the user are valid.

  1. Initial State Validation: The test confirms that the login button is initially disabled when no inputs are provided.

  2. Partial Input Validation: The test simulates entering invalid email and password combinations step-by-step to ensure that the button remains disabled until all conditions for validity are met.

  3. Valid Input Validation: Finally, the test validates that the login button becomes enabled only when both the email and password meet the required validation criteria (a valid email format and a password of sufficient length).

This test ensures that the Login composable correctly enforces input validation and enables the login button only under valid conditions.

System Under Test

@Composable
fun Login(onSuccess: (email: Email) -> Unit, viewModel: LoginViewModel = hiltViewModel()) {

  LaunchedEffect(key1 = viewModel.loginState, block = {
    if (viewModel.loginState == LoginState.LoginSuccess) onSuccess(viewModel.email)
  })

  Column {
    Text(text = stringResource(id = R.string.login))

    EmailInput(modifier = Modifier
      .semantics { testTagsAsResourceId = true;testTag = "emailInput" }
      .testTag("emailInput")
      .fillMaxWidth(),
      value = viewModel.email.value ?: "",
      isEnabled = viewModel.loginState !== LoginState.InProgress,
      onValueChange = viewModel::updateEmail)

    PasswordInput(modifier = Modifier
      .semantics { testTagsAsResourceId = true;testTag = "passwordInput" }
      .fillMaxWidth(),
      value = viewModel.password.value ?: "",
      isEnabled = viewModel.loginState !== LoginState.InProgress,
      onValueChange = viewModel::updatePassword)

    if (viewModel.loginState === LoginState.LoginPending){
        PrimaryButton(modifier = Modifier
            .semantics { testTagsAsResourceId = true;testTag = "loginButton" }
            .fillMaxWidth(),
            text = stringResource(id = R.string.login),
            enabled = viewModel.isLoginButtonEnabled,
            onClick = viewModel::login)
    }

    if (viewModel.loginState === LoginState.InProgress){
        CircularProgressIndicator(
            modifier = Modifier
                .semantics { testTagsAsResourceId = true;testTag = "progressLoader" }
                .align(Alignment.CenterHorizontally)
        )
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Test

  • This is a Compose UI unit test that runs on the JVM. Therefore, the code must be placed inside app/src/test/java/../LoginKtTest.kt .
@RunWith(RobolectricTestRunner::class)
class LoginKtTest {

  @get:Rule
  val composeRule = createComposeRule()

  @get:Rule
  var mainCoroutineRule = MainCoroutineRule()

  @Test
  fun shouldEnableButtonOnlyWhenInputsAreValid() {
    with(composeRule) {
      val loginUseCase = mockk<LoginUseCaseImpl>()
      val loginViewModel = LoginViewModel(loginUseCase)
      setContent { Login(onSuccess = {}, viewModel = loginViewModel) }
      onNodeWithTag("loginButton").assertIsNotEnabled()

      onNodeWithTag("emailInput").performTextInput("abcd")
      onNodeWithTag("loginButton").assertIsNotEnabled()

      onNodeWithTag("emailInput").performTextInput("[email protected]")
      onNodeWithTag("loginButton").assertIsNotEnabled()

      onNodeWithTag("passwordInput").performTextInput("12")
      onNodeWithTag("loginButton").assertIsNotEnabled()

      onNodeWithTag("passwordInput").performTextInput("12345")
      onNodeWithTag("loginButton").assertIsEnabled()
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Dependencies

// Needed for createComposeRule , createAndroidComposeRule and other rules used to perform UI test
testImplementation("androidx.compose.ui:ui-test-junit4:$compose_version") // used with robolectric to run ui test on jvm

// Needed for createComposeRule(), but not for createAndroidComposeRule<YourActivity>():
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")

// Dependency injection for For instrumented tests on JVM
testImplementation("com.google.dagger:hilt-android-testing:2.49")
kaptTest("com.google.dagger:hilt-compiler:2.49")

// Needed to run android UI test on JVM instead of on an emulator or device
testImplementation("org.robolectric:robolectric:4.10.3)

// Helper for other arch dependencies, including JUnit test rules that can be used with LiveData, coroutines etc
testImplementation("androidx.arch.core:core-testing:2.2.0")
Enter fullscreen mode Exit fullscreen mode

Setup

testOptions {
        unitTests {
            // Enables unit tests to use Android resources, assets, and manifests.
            isIncludeAndroidResources = true
            // Whether unmocked methods from android.jar should throw exceptions or return default values (i.e. zero or null).
            isReturnDefaultValues = true
        }
}
Enter fullscreen mode Exit fullscreen mode

Command

./gradlew testDebugUnitTest
Enter fullscreen mode Exit fullscreen mode

Source Code


Next Article


Test Your Code, Rest Your Worries

With a sturdy suite of tests as steadfast as a fortress, developers can confidently push code even on a Friday evening and log off without a trace of worry.

compose Article's
30 articles in total
Favicon
Getting Started with Android Testing: Building Reliable Apps with Confidence (Part 3/3)
Favicon
Getting Started with Android Testing: Building Reliable Apps with Confidence (Part 2/3)
Favicon
Our experience becoming a Compose-first app
Favicon
Building a subscription tracker Desktop and iOS app with compose multiplatform — Offline data
Favicon
Mastering @Stable in Jetpack Compose for Better UI Performance
Favicon
Do you still use version in Docker compose?
Favicon
Building a subscription tracker Desktop and iOS app with compose multiplatform—Providing feedbacks
Favicon
Self-host - Part 2 - Zero-Downtime Deployment using Docker Swarm
Favicon
Building a subscription tracker Desktop and iOS app with compose multiplatform - Configuring Notion
Favicon
How to create LazyColumn with drag and drop elements in Jetpack Compose. Part 1.
Favicon
Retro on "Docker Compose for Developers"
Favicon
CountryCodePicker in Compose Multiplatform for Android and iOS
Favicon
Coil and Ktor in Kotlin Multiplatform Compose project
Favicon
Adaptar Kotlin 2.0 en aplicaciones Android
Favicon
Building a subscription tracker Desktop and iOS app with compose multiplatform - Setup
Favicon
30-days plan to master Jetpack Compose with resources and three practice projects
Favicon
CMPPreference - Compose Multiplatform
Favicon
Introducing Compose Multiplatform Media Player: Your Go-To Solution for Seamless Media Playback
Favicon
QRKit — QRCode Scanning in Compose Multiplatform for Android and iOS
Favicon
SDP-SSP-Compose-Multiplatform
Favicon
Using docker compose watch with Node.js
Favicon
SharedPreferences Debugging: Unlocking the Developer Mode with Prefixer
Favicon
Navigation in Compose Multiplatform with Animations
Favicon
Introducing Backgroundable - Your Wallpaper Companion! 🌟
Favicon
Kilua - new Kotlin/Wasm web framework
Favicon
I've just open sourced N8
Favicon
One Minute: Compose
Favicon
State using Jetpack Compose
Favicon
compose middleware
Favicon
How to create my first Jetpack Compose Android App

Featured ones: