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
false
for them (e.g., missing domain, misplaced characters).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()
}
}
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
mockk
to mock theSavedStateHandle
andLogoutUseCase
, which are dependencies in theProfileViewModel
.Testing Behavior: The mock for
SavedStateHandle
is configured to return a predefined email addressabcd@gmail.com
when theKEY_EMAIL
key is accessed.Validation: After initializing the
ProfileViewModel
, we assert that theemailAddress
property 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
SavedStateHandle
to simulate retrieving theemail
address from the saved state, and we stub theLogoutUseCase
to simulate a successful logout without performing the actual logic.Act:: Triggering Logout: The
logout
function is called on theProfileViewModel
, and the coroutine is run to completion usingrunCurrent()
.Assert: The test asserts that after calling
logout
, theshouldLogout
state is updated totrue
and that thelogout
function was successfully called, as indicated by theisLogoutSuccess
flag 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)