DEV Community

Sridhar Subramani
Sridhar Subramani

Posted on

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

Learn the Fundamentals of Android Testing, One Step at a Time Part 1/3


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.


Different Types of Test

The following are the primary testing types that are commonly used in software products:

  • Unit testing
  • UI testing
  • Integration testing

Place of Execution

Test Execution
Unit testing JVM
UI testing JVM or Android device
Integration testing Android device

Unit Testing

Unit testing usually refers to testing a particular unit of code in complete isolation from other components to ensure its correctness and functionality. Developers often use frameworks like Mockito to create stubs (test doubles), mocks, etc., to achieve this isolation.

  • Stub: A stub is a direct replacement for a function, interface, or abstract class (or any other dependency). It
    allows us to swap the original implementation with a test-specific version, often referred to as a test dummy (or test
    double).

  • Mock: A mock serves as a more advanced test double for a dependency. Mocking frameworks let us actively simulate different behaviours by configuring the mock to return specific responses based on inputs or conditions. Furthermore, mocks allow us to confirm interactions by verifying the existence of a method, its number of calls, and the arguments passed during each call.

  • Why do we need this? During testing, especially unit testing, we aim to isolate the component under test from its dependencies. This ensures that we're testing the component alone, making the tests simpler, faster, and less error-prone. Mocking or stubbing helps us avoid injecting side effects or relying on external dependencies.

  • Example: Imagine a ViewModel class that depends on a repository. The Repository class, in turn, makes network API calls. If we want to write a unit test for the ViewModel alone, we don’t want to incur the overhead of making actual API calls, as this can make the test error-prone due to network conditions or server response times. To avoid these side effects, we can replace the repository with a stub (test double) or a mock during the test. This ensures that we focus only on the behaviour of the ViewModel while bypassing external dependencies.

Famous Unit Testing Frameworks

Framework Description
Junit Testing framework for Java
Mockito Mocking framework for unit tests written in Java/Kotlin
Truth To perform assertions in tests

Simple Test Without Mocks

In this test suite, we are validating the behavior of the isValid() method in the Email class. The isValid()
method checks whether the email provided is a valid email address or not. We are testing three key scenarios:

  1. Null Email: Verifying that when the email value is null, the method returns false.

  2. Invalid Email: Checking various invalid email formats to ensure that the method correctly returns false for them (e.g., missing domain, misplaced characters).

  3. Valid Email: Confirming that the method correctly returns true for properly formatted email addresses.

Each test ensures the isValid() method behaves as expected under different conditions, guaranteeing that the email validation works correctly.

System Under Test

data class Email(val value: String?) : Parcelable {
    fun isValid(): Boolean {
        return if (value == null) false else PatternsCompat.EMAIL_ADDRESS.matcher(value).matches()
    }
}
Enter fullscreen mode Exit fullscreen mode

Test

class EmailTest {

    @Test
    fun shouldReturnIsValidAsFalseWhenEmailIsNull() {
        Truth.assertThat(Email(null).isValid()).isFalse()
    }

    @Test
    fun shouldReturnIsValidAsFalseWhenEmailIsInvalid() {
        Truth.assertThat(Email("aa@.com").isValid()).isFalse()
        Truth.assertThat(Email("aacd@aa.com@").isValid()).isFalse()
        Truth.assertThat(Email("").isValid()).isFalse()
        Truth.assertThat(Email("@gmail.com").isValid()).isFalse()
    }

    @Test
    fun shouldReturnIsValidAsTrueWhenEmailIsValid() {
        Truth.assertThat(Email("abcd@domain.com").isValid()).isTrue()
        Truth.assertThat(Email("a@domain.in").isValid()).isTrue()
    }
}
Enter fullscreen mode Exit fullscreen mode

Simple Test With Mocks

In this test, we are verifying the behavior of the ProfileViewModel class, specifically the retrieval of the email address from the SavedStateHandle. The test mocks the SavedStateHandle to simulate retrieving an email address from the saved state.

  1. Mocking Dependencies: We use mockk to mock the SavedStateHandle and LogoutUseCase, which are dependencies in the ProfileViewModel.

  2. Testing Behavior: The mock for SavedStateHandle is configured to return a predefined email address abcd@gmail.com when the KEY_EMAIL key is accessed.

  3. Validation: After initializing the ProfileViewModel, we assert that the emailAddress property correctly retrieves the mocked email value from the SavedStateHandle.

This test ensures that the ProfileViewModel correctly reads the email address from the saved state during its initialization.

System Under Test

@HiltViewModel
class ProfileViewModel @Inject constructor(
  savedStateHandle: SavedStateHandle,
  private val logoutUseCase: LogoutUseCase
) : ViewModel() {
  val emailAddress = savedStateHandle.get<String>(BundleArgs.KEY_EMAIL)
}
Enter fullscreen mode Exit fullscreen mode

Test

@Test
fun `should return email value from saved state handle when email address is read from viewModel`() {
  val savedStateHandleMock = mockk<SavedStateHandle>()
  every<String?> { savedStateHandleMock[BundleArgs.KEY_EMAIL] } returns "abcd@gmail.com"

  val logoutUseCase = mockk<LogoutUseCase>()

  val profileViewModel = ProfileViewModel(savedStateHandleMock, logoutUseCase)
  assertThat(profileViewModel.emailAddress).isEqualTo("abcd@gmail.com")
}
Enter fullscreen mode Exit fullscreen mode

Test With Mocks and Stubs

In this test, we are testing the behavior of the ProfileViewModel class when the logout function is called, ensuring that the logout process is correctly triggered and the shouldLogout state is updated.

Let me use AAA test pattern to explain the test case. AAA stands for Arrange, Act, and Assert.

  1. Arrange:: Mocking and Stub Dependencies: We mock the SavedStateHandle to simulate retrieving the email address from the saved state, and we stub the LogoutUseCase to simulate a successful logout without performing the actual logic.

  2. Act:: Triggering Logout: The logout function is called on the ProfileViewModel, and the coroutine is run to completion using runCurrent().

  3. Assert: The test asserts that after calling logout, the shouldLogout state is updated to true and that the logout function was successfully called, as indicated by the isLogoutSuccess flag being true.

This test ensures that the ProfileViewModel correctly handles the logout process, updating the appropriate states and interacting with the LogoutUseCase.

System Under Test

@HiltViewModel
class ProfileViewModel @Inject constructor(
  savedStateHandle: SavedStateHandle,
  private val logoutUseCase: LogoutUseCase
) : ViewModel() {
  val emailAddress = savedStateHandle.get<String>(BundleArgs.KEY_EMAIL)

  var shouldLogout by mutableStateOf(false)
    private set

  fun logout() {
    viewModelScope.launch {
      logoutUseCase.logout(Email(emailAddress))
      shouldLogout = true
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Test

@Test
fun `should call logout callback when logout button is pressed`() = runTest(testDispatcher) {
    Dispatchers.setMain(testDispatcher)

    // Arrange    
    val savedStateHandleMock = mockk<SavedStateHandle>()
    every<String?> { savedStateHandleMock[BundleArgs.KEY_EMAIL] } returns "abcd@gmail.com"

    var isLogoutSuccess = false
    val logoutStub = object : LogoutUseCase {
      override suspend fun logout(email: Email) {
        isLogoutSuccess = true
      }
    }

    val profileViewModel = ProfileViewModel(savedStateHandleMock, logoutStub)

    // Act    
    profileViewModel.logout()
    runCurrent()  // run current co routine to completion

    // Assert    
    assertThat(profileViewModel.shouldLogout).isTrue()
    assertThat(isLogoutSuccess).isTrue()
  }
Enter fullscreen mode Exit fullscreen mode

Dependencies

// Regular JUnit dependency
testImplementation("junit:junit:4.13.2")

// Assertion library
testImplementation("com.google.truth:truth:1.1.4")

// Allows us to create and configure mock objects, stub methods, verify method invocations, and more
testImplementation("io.mockk:mockk:1.13.5")
Enter fullscreen mode Exit fullscreen mode

Command

./gradlew testDebugUnitTest
Enter fullscreen mode Exit fullscreen mode

Source Code


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.


Next Article

Top comments (0)