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:
Null Email: Verifying that when the email value is
null, the method returnsfalse.Invalid Email: Checking various invalid email formats to ensure that the method correctly returns
falsefor them (e.g., missing domain, misplaced characters).Valid Email: Confirming that the method correctly returns
truefor 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()
}
}
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()
}
}
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.
Mocking Dependencies: We use
mockkto mock theSavedStateHandleandLogoutUseCase, which are dependencies in theProfileViewModel.Testing Behavior: The mock for
SavedStateHandleis configured to return a predefined email addressabcd@gmail.comwhen theKEY_EMAILkey is accessed.Validation: After initializing the
ProfileViewModel, we assert that theemailAddressproperty correctly retrieves the mocked email value from theSavedStateHandle.
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)
}
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")
}
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.
Arrange:: Mocking and Stub Dependencies: We mock the
SavedStateHandleto simulate retrieving theemailaddress from the saved state, and we stub theLogoutUseCaseto simulate a successful logout without performing the actual logic.Act:: Triggering Logout: The
logoutfunction is called on theProfileViewModel, and the coroutine is run to completion usingrunCurrent().Assert: The test asserts that after calling
logout, theshouldLogoutstate is updated totrueand that thelogoutfunction was successfully called, as indicated by theisLogoutSuccessflag beingtrue.
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
}
}
}
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()
}
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")
Command
./gradlew testDebugUnitTest
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.
Top comments (0)