In modern Android development, building robust apps goes far beyond just writing code. It’s about delivering high-quality features in iterative cycles with the confidence that everything works as expected. This article walks you through a full feature development cycle in Android using Kotlin, Jetpack Compose, and modern testing practices.
We’ll build a simple feature: a button that fetches and displays user data. You'll learn how to:
-
Structure code using ViewModel and Repository
-
Write unit and UI tests
-
Integrate automated testing in CI/CD pipelines
Let’s dive in
Full Cycle with Testing - Detailed Steps
Step 1: Set up your environment and dependencies
Before starting, ensure your project has the following dependencies in build.gradle
(Module-level):
dependencies {
implementation "androidx.compose.ui:ui:1.3.0"
implementation "androidx.compose.material3:material3:1.0.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
testImplementation "junit:junit:4.13.2"
testImplementation "org.mockito:mockito-core:4.0.0"
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.3.0"
androidTestImplementation "androidx.test.ext:junit:1.1.5"
androidTestImplementation "androidx.test.espresso:espresso-core:3.5.0"
}
Step 2: Implement the Feature
Let’s assume the feature involves a button that, when clicked, fetches user data from an API.
2.1: Create the User
Data Model
data class User(val id: Int, val name: String)
2.2: Create the UserRepository
to Fetch Data
class UserRepository {
// Simulate network request with delay
suspend fun fetchUserData(): User {
delay(1000) // Simulating network delay
return User(id = 1, name = "John Doe")
}
}
2.3: Create the UserViewModel
to Expose Data
class UserViewModel(private val repository: UserRepository) : ViewModel() {
private val _userData = MutableLiveData<User?>()
val userData: LiveData<User?> get() = _userData
fun fetchUser() {
viewModelScope.launch {
_userData.value = repository.fetchUserData()
}
}
}
2.4: Create the UserScreen
Composable to Display Data
@Composable
fun UserScreen(viewModel: UserViewModel) {
val user by viewModel.userData.observeAsState()
Column(modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = { viewModel.fetchUser() }) {
Text("Fetch User")
}
user?.let {
Text(text = "User: ${it.name}")
} ?: Text(text = "No user fetched yet")
}
}
Step 3: Write Unit Tests
3.1: Unit Test for UserRepository
Write a simple unit test to test that UserRepository
correctly fetches user data.
@RunWith(MockitoJUnitRunner::class)
class UserRepositoryTest {
private lateinit var repository: UserRepository
@Before
fun setup() {
repository = UserRepository()
}
@Test
fun testFetchUserData() = runBlocking {
val result = repository.fetchUserData()
assertEquals(1, result.id)
assertEquals("John Doe", result.name)
}
}
3.2: Unit Test for UserViewModel
Next, test that the UserViewModel
correctly interacts with the repository and updates the UI state.
class UserViewModelTest {
private lateinit var viewModel: UserViewModel
private lateinit var repository: UserRepository
@Before
fun setup() {
repository = mock(UserRepository::class.java)
viewModel = UserViewModel(repository)
}
@Test
fun testFetchUser() = runBlocking {
val user = User(1, "John Doe")
`when`(repository.fetchUserData()).thenReturn(user)
viewModel.fetchUser()
val value = viewModel.userData.getOrAwaitValue() // Use LiveData testing extensions
assertEquals(user, value)
}
}
Step 4: Write UI Tests with Jetpack Compose
4.1: Write Compose UI Test
Use ComposeTestRule
to test the UserScreen
composable.
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testFetchUserButton_click() {
val viewModel = UserViewModel(UserRepository())
composeTestRule.setContent {
UserScreen(viewModel)
}
// Assert that the UI initially shows the "No user fetched yet" text
composeTestRule.onNodeWithText("No user fetched yet").assertIsDisplayed()
// Simulate button click
composeTestRule.onNodeWithText("Fetch User").performClick()
// After clicking, assert that "User: John Doe" is displayed
composeTestRule.onNodeWithText("User: John Doe").assertIsDisplayed()
}
4.2: Write Espresso Test for Hybrid UI (if needed)
In case you’re testing a hybrid UI (Jetpack Compose + XML Views), you can also use Espresso.
@Test
fun testFetchUserButton_click() {
onView(withId(R.id.fetchUserButton)).perform(click())
onView(withId(R.id.resultText)).check(matches(withText("User: John Doe")))
}
Step 5: Run All Tests in CI/CD
-
Set up GitHub Actions or Jenkins to automatically run the tests on every push to the repository.
Here’s an example configuration for GitHub Actions:
name: Android CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v2
with:
java-version: '11'
- name: Build and test
run: ./gradlew build testDebugUnitTest connectedDebugAndroidTest
Step 6: Code Review and Refactor
With your code and tests in place:
-
Refactor for better architecture
-
Add new states like loading and error
-
Add tests for edge cases
-
Merge changes with confidence
-
Release to staging/production
Step 7: Deploy to Staging or Production
After successful tests, deploy the feature to a staging environment or directly to production, depending on your CI/CD pipeline.
Key Takeaways:
-
Iterative development: Implement small, testable features and enhance them with every iteration.
-
Automate testing: Unit tests, UI tests, and integration tests should be automated and run on every code change.
-
Focus on user behavior: Write tests that mimic real user behavior (e.g., pressing buttons and verifying UI changes).
-
Continuous integration: Set up CI/CD pipelines to run tests automatically and ensure the stability of your app at all times.
By following this cycle, you ensure that the app is always in a deployable state, with tests proving that each feature works as expected.
Final Thoughts
Iterative development backed by strong testing practices ensures you can deliver robust Android features without fear of breaking the app. With tools like Jetpack Compose, JUnit, and CI/CD, it’s never been easier to ship confidently.
๐ก “The best code is not only written, it's verified and trusted by tests.”
๐ข Feedback: Did you find this article helpful? Let me know your thoughts or suggestions for improvements! ๐ please leave a comment below. I’d love to hear from you! ๐
Happy coding! ๐ป✨
0 comments:
Post a Comment