DEV Community

Sridhar Subramani
Sridhar Subramani

Posted on

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("abcd@gmail.com")
      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.

Top comments (0)