Showing posts with label CI/CD. Show all posts
Showing posts with label CI/CD. Show all posts

Shipping High-Quality Android Code with Testing in Iterative Cycles (Kotlin + Jetpack Compose)

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:

  1. Iterative development: Implement small, testable features and enhance them with every iteration.

  2. Automate testing: Unit tests, UI tests, and integration tests should be automated and run on every code change.

  3. Focus on user behavior: Write tests that mimic real user behavior (e.g., pressing buttons and verifying UI changes).

  4. 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! ๐Ÿ’ป✨

Implementing Continuous Integration (CI) for Android Applications

 Continuous Integration (CI) is an essential practice in modern software development that helps ensure code quality, minimize integration issues, and deliver a stable product. In this article, we will discuss the importance of CI for Android development and provide a step-by-step guide to implement a CI pipeline effectively, complete with code examples.



Why Continuous Integration is Crucial for Android Apps

CI enables developers to merge their code changes into a shared repository multiple times a day. By running automated tests and builds with each integration, CI ensures that errors are caught early in the development process, preventing them from accumulating and becoming difficult to resolve.

Key benefits of CI for Android apps include:

  • Automated Testing: Ensures new code doesn’t break existing functionality.

  • Early Issue Detection: Detects integration issues early, reducing time-consuming debugging sessions.

  • Better Collaboration: Allows developers to work collaboratively without worrying about breaking changes.

  • Fast Feedback: Provides fast feedback to developers, allowing them to address problems quickly.

Tools for CI in Android Development

To implement CI for Android, there are several tools and services available:

  1. Jenkins: A popular, open-source automation server that is highly configurable.

  2. GitHub Actions: CI/CD workflows directly integrated with GitHub repositories.

  3. Bitrise: A cloud-based CI/CD service that is designed specifically for mobile applications.

  4. CircleCI: Known for its fast performance and easy integration with GitHub and Bitbucket.

  5. GitLab CI: CI/CD integration available for projects hosted on GitLab.

Step-by-Step Guide to Set Up CI for an Android Project

Step 1: Configure Version Control

Start by configuring your version control system. Git is widely used for Android projects, and CI pipelines are typically triggered by changes in the Git repository. Consider using platforms like GitHub, Bitbucket, or GitLab to host your repository.

Step 2: Choose a CI Tool

Select a CI tool based on your project needs. For this guide, let’s use GitHub Actions to set up CI.

Step 3: Define Your Build Workflow

In GitHub Actions, you define workflows using a YAML file placed in the .github/workflows/ directory within your repository. Here’s a sample configuration for an Android project:

name: Android CI

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v2

    - name: Set up JDK 11
      uses: actions/setup-java@v2
      with:
        distribution: 'zulu'
        java-version: '11'

    - name: Cache Gradle files
      uses: actions/cache@v2
      with:
        path: |
          ~/.gradle/caches
          ~/.gradle/wrapper
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
        restore-keys: |
          ${{ runner.os }}-gradle-

    - name: Build with Gradle
      run: ./gradlew build

    - name: Run Unit Tests
      run: ./gradlew test

    - name: Lint Checks
      run: ./gradlew lint

CI builds require that all dependencies are defined in the build.gradle file. Make sure all dependencies are resolved through repositories like Maven Central or JitPack so that the CI tool can download them without manual intervention.

Step 5: Run Tests

Testing is a critical component of CI. Make sure to include unit tests (using JUnit), UI tests (using Espresso), and integration tests as part of your build pipeline. The workflow above includes a step to run unit tests with Gradle.

Step 6: Code Quality Checks

Tools like Detekt (for Kotlin) or Lint can be added to the CI pipeline to enforce coding standards and ensure code quality. Adding a lint check step will help identify any code issues before merging.

Step 7: Configure Notifications

Configure notifications to keep the team informed about build status. GitHub Actions will show the status of the build on pull requests, and you can also set up Slack or email notifications for build failures.

Best Practices for CI in Android Projects

  1. Keep Builds Fast: Use caching to reduce build times. Cache Gradle dependencies and any other artifacts that take time to generate.

  2. Run Tests in Parallel: For faster feedback, configure the CI pipeline to run unit tests and UI tests in parallel.

  3. Automate Everything: Automate not just builds and tests, but also code quality checks, deployment, and versioning.

  4. Fail Fast: Ensure that the build fails immediately upon detecting an issue. This makes debugging easier and prevents cascading errors.

  5. Use Emulator Snapshots: For UI testing, use emulator snapshots to reduce the time required to boot up emulators.

Conclusion

Implementing Continuous Integration for Android applications helps ensure the stability, reliability, and quality of your app. With tools like GitHub Actions, Jenkins, and Bitrise, you can create automated pipelines that continuously verify the health of your project. By following best practices, you can streamline development, improve collaboration, and ultimately deliver a better product to your users.

By implementing these steps, you can create a reliable and efficient CI pipeline for your Android applications, making your development process smoother and your final product more robust.

#Kotlin #Code4Kotlin #CI