Chapter 8: Testing Your App

The "Why": What is Software Testing?

Writing code that works *once* is easy. Writing code that *keeps* working after you add new features, fix other bugs, or refactor (clean up) old code is **hard**. This is what separates a hobbyist from a professional engineer.

Software Testing is the process of creating small, automatic programs that *verify* your main code works as expected. You write a "test" that checks a "feature." If the test passes, you know the feature works. If it fails, you know you broke something.

The Value of an Automated Test Suite

Imagine your app has a complex login screen. You add a new "Forgot Password" feature. How do you know you didn't accidentally break the "Login" button or the "Sign Up" button?

  • The Bad Way (Manual Testing): You run the app. You type a valid email/password. You click login. It works. You type an invalid email. You click login. It shows an error. You type a valid email but wrong password... you get the idea. This is slow, boring, and you *will* miss a case.
  • The Good Way (Automated Testing): You run one command in your terminal: ./gradlew test. In 5 seconds, 500 tests run automatically, and you get a green checkmark. You know, with confidence, that you broke nothing.

Tests are your **safety net**. They give you the confidence to change and improve your code without fear.

The Testing Pyramid

This is the most important theory in software testing. It describes the *types* of tests you should write and *how many* of each you should have. A healthy app has a *lot* of small, fast tests at the bottom and a *few* big, slow tests at the top.

[Image of the Testing Pyramid (UI Tests at top, Integration Tests in middle, Unit Tests at bottom)]
  1. Unit Tests (70%):
    • What: Tests a single, isolated "unit" (one function or one class).
    • Speed: Extremely fast (milliseconds).
    • Where: Runs on your computer's JVM (in the /src/test folder).
    • Example: "Does the validateEmail("test@") function return false?"
  2. Integration Tests (20%):
    • What: Tests how two or more units "integrate" and work together.
    • Speed: Medium (seconds).
    • Where: Can run on the JVM or on an Android device (in /src/androidTest).
    • Example: "If I save a User to the Room Database, can I then fetch it correctly?" (Tests the DAO + Database).
  3. End-to-End (E2E) / UI Tests (10%):
    • What: Tests a complete user flow from start to finish.
    • Speed: Very slow (minutes).
    • Where: *Must* run on a real device or emulator (in /src/androidTest).
    • Example: "Launch the app, type 'user' in the text field, type '1234' in the password field, click 'Login', and check if the 'Welcome, user!' text appears."

We will learn to write all three types.

Part 1: Unit Tests (Local Tests)

Unit tests are the foundation of your testing strategy. They are small, fast, and test a single piece of logic in isolation. They run on your local computer's JVM (Java Virtual Machine), not on an Android device, which is why they are so fast.

Test Location

In your Android Studio project panel, you have three main source folders:

  • src/main/java: Your main app code.
  • src/test/java: Your **Unit Tests** go here.
  • src/androidTest/java: Your **Integration & UI Tests** go here.

Step 1: Add Dependencies

The standard library for testing in the Java/Kotlin world is **JUnit**. Android projects use **JUnit 4** by default (though JUnit 5 is also becoming popular).

In your build.gradle.kts (Module: :app), these lines should already be there:

dependencies {
    // ...
    testImplementation(libs.junit) // JUnit 4
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
}

The testImplementation line is for your local unit tests. androidTestImplementation is for tests that run on a device.

Step 2: The "Arrange, Act, Assert" (AAA) Pattern

All unit tests should follow the "AAA" pattern to be clean and readable.

  1. Arrange: Set up your test. Initialize your objects and prepare the "world" for the one thing you want to test.
  2. Act: Perform the single action. Call the one function you are testing.
  3. Assert: Check the result. Verify that the action you performed had the expected outcome.

Step 3: Writing Your First Unit Test

Let's imagine we have a simple Validator class in our main code.

src/main/java/com/example/myapp/util/Validator.kt
object Validator {
    fun isValidPassword(password: String): Boolean {
        return password.length >= 8
    }
}

Now, let's write a test for it in the /src/test folder. By convention, the test file is named after the class it's testing, with "Test" at the end.

src/test/java/com/example/myapp/util/ValidatorTest.kt
import org.junit.Test
import org.junit.Assert.*

class ValidatorTest {
    
    // The @Test annotation tells JUnit this is a test function
    @Test
    fun `isValidPassword returns false for short password`() {
        // We can use backticks `` for long, descriptive function names
        
        // 1. Arrange
        val shortPassword = "123"
        
        // 2. Act
        val result = Validator.isValidPassword(shortPassword)
        
        // 3. Assert
        assertFalse("Password '123' should be invalid", result)
    }
    
    @Test
    fun `isValidPassword returns true for long password`() {
        // Arrange
        val longPassword = "12345678"
        
        // Act
        val result = Validator.isValidPassword(longPassword)
        
        // Assert
        assertTrue("Password '12345678' should be valid", result)
    }
}

You can run this test by clicking the small green "play" icon next to the class name in Android Studio.

Common JUnit Assertions

  • assertEquals(expected, actual): Checks if two values are equal.
  • assertNotEquals(unexpected, actual): Checks if two values are *not* equal.
  • assertTrue(message, condition): Checks if a condition is true.
  • assertFalse(message, condition): Checks if a condition is false.
  • assertNull(object): Checks if an object is null.
  • assertNotNull(object): Checks if an object is *not* null.
Read More about JUnit 4 →

Part 2: Mocking with Mockito

The Validator test was easy because it had no **dependencies**. But what about our NotesViewModel from Chapter 7? It *depends* on a NotesRepository.
When we unit test the ViewModel, we should *only* test the ViewModel's logic, not the Repository's logic. We need to **isolate** the ViewModel.
We do this by creating a **Mock** (a "fake" version) of its dependency.

Mocking is the process of creating a fake, controllable object that pretends to be a real dependency. We can tell this mock object, "When your getNotes() function is called, return this fake list of notes."

The most popular mocking library for Kotlin is **Mockito-Kotlin**.

Step 1: Add Mockito Dependency

dependencies {
    // ...
    testImplementation(libs.junit)
    
    // Add Mockito
    testImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0")
    testImplementation("org.mockito:mockito-inline:5.2.0") // For mocking final classes
}

Step 2: Mocking in a Test

Let's imagine we have this ViewModel and Repository:

src/main/java/.../NotesRepository.kt
// This is an interface, which is easy to mock
interface NotesRepository {
    suspend fun getAllNotes(): List<String>
}

class NotesViewModel(val repository: NotesRepository) : ViewModel() {
    
    private val _notes = MutableStateFlow(emptyList<String>())
    val notes: StateFlow<List<String>> = _notes.asStateFlow()
    
    fun fetchNotes() {
        viewModelScope.launch {
            _notes.value = repository.getAllNotes()
        }
    }
}

Now, let's test this ViewModel. We need to create a **fake** NotesRepository.

src/test/java/.../NotesViewModelTest.kt
import org.junit.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.mockito.kotlin.verify
import kotlinx.coroutines.runTest // For testing coroutines
import org.junit.Assert.*

class NotesViewModelTest {

    // 1. Create a mock object for the dependency
    private val mockRepository: NotesRepository = mock()

    // 2. Create the class we want to test (the "Subject Under Test")
    private val viewModel = NotesViewModel(mockRepository)

    @Test
    fun `fetchNotes updates notes StateFlow`() = runTest { // Use runTest for coroutines
        // --- Arrange ---
        val fakeNotes = listOf("Note 1", "Note 2")
        
        // "WHEN-EVER" repository.getAllNotes() is called, "THEN RETURN" our fake list
        whenever(mockRepository.getAllNotes()).thenReturn(fakeNotes)
        
        // --- Act ---
        viewModel.fetchNotes()
        
        // --- Assert ---
        // Check that the ViewModel's state was updated
        assertEquals(fakeNotes, viewModel.notes.value)
        
        // (Optional) Verify that the repository function was *actually* called
        verify(mockRepository).getAllNotes()
    }
}
Read More about Mockito-Kotlin →

Testing Coroutines & `Dispatchers`

The test above has a problem. The viewModelScope in NotesViewModel uses Dispatchers.Main, which is not available in a local unit test. Your test will crash.

To fix this, we need to **replace** the real Dispatchers.Main with a fake TestDispatcher during our test.

This is a very advanced but *essential* part of testing ViewModels. We create a "JUnit Rule" to do this automatically for every test.

src/test/java/com/example/myapp/MainCoroutineRule.kt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description

// This is a JUnit Rule. You can copy/paste this file into your project.
class MainCoroutineRule(
    val testDispatcher: TestDispatcher = StandardTestDispatcher()
) : TestWatcher() {
    
    override fun starting(description: Description) {
        // 1. BEFORE the test, set the Main dispatcher to our fake TestDispatcher
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description) {
        // 2. AFTER the test, clean up
        Dispatchers.resetMain()
    }
}

Now, we just add this "Rule" to our test class:

src/test/java/.../NotesViewModelTest.kt (Updated)
import org.junit.Rule
import kotlinx.coroutines.test.runTest

class NotesViewModelTest {

    // Add this @get:Rule. This will run before every @Test
    @get:Rule
    val mainCoroutineRule = MainCoroutineRule()

    // ...
    private val mockRepository: NotesRepository = mock()
    private val viewModel = NotesViewModel(mockRepository)

    @Test
    fun `fetchNotes updates notes StateFlow`() = runTest { // This runTest uses the TestDispatcher
        // ... (Arrange, Act, Assert from before) ...
    }
}

Part 3: Integration Tests (Instrumented Tests)

Unit tests are great, but sometimes you need to test how components work *together*. The most common integration test is **testing your Room Database**.
Does your @Query("SELECT * ...") actually work? Does it crash?
These tests must run on an Android device (real or emulator) because the Room database is an Android-specific component. These tests live in src/androidTest/java.

Testing the Room DAO

We don't want to test on our *real* app database, as that would fill it with junk. Room provides a special inMemoryDatabaseBuilder that creates a temporary, "fake" database just for the test.

Step 1: Add Dependencies

dependencies {
    // ...
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
    
    // Add Room testing library
    androidTestImplementation("androidx.room:room-testing:$room_version")
}

Step 2: Write the DAO Test

src/androidTest/java/.../NoteDaoTest.kt
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.io.IOException
import org.junit.Assert.*

@RunWith(AndroidJUnit4::class) // Tell JUnit to run this on Android
class NoteDaoTest {
    
    private lateinit var noteDao: NoteDao
    private lateinit var db: AppDatabase

    // @Before runs *before* every single @Test
    @Before
    fun createDb() {
        val context = ApplicationProvider.getApplicationContext<Context>()
        // Create a fake, in-memory database that lives only in RAM
        db = Room.inMemoryDatabaseBuilder(
            context, AppDatabase::class.java)
            .allowMainThreadQueries() // Only for testing!
            .build()
        noteDao = db.noteDao()
    }

    // @After runs *after* every single @Test
    @After
    @Throws(IOException::class)
    fun closeDb() {
        db.close()
    }

    @Test
    @Throws(Exception::class)
    fun `insertAndGetNote`() = runBlocking { // Use runBlocking for simple suspend tests
        // Arrange
        val note = Note(title = "Test", content = "This is a test")
        
        // Act
        noteDao.insert(note)
        val allNotes = noteDao.getAllNotes().first() // .first() gets the first value from the Flow
        
        // Assert
        assertEquals(allNotes[0].title, "Test")
    }
}

Part 4: UI Tests (End-to-End)

Finally, we have UI tests. These launch your *actual* app on an emulator and simulate a user. They are slow, brittle (can break easily), but they are the only way to verify that everything works together.

We have two different frameworks for this, depending on your UI system.

Option A: Espresso (The Legacy XML Way)

Espresso is the standard library for testing XML-based UIs. It has a simple "View - Action - Assertion" API.

// In src/androidTest/java/.../MainActivityEspressoTest.kt
@RunWith(AndroidJUnit4::class)
class MainActivityEspressoTest {

    // This rule automatically launches MainActivity before each test
    @get:Rule
    val activityRule = ActivityScenarioRule(MainActivity::class.java)

    @Test
    fun `clickButton_changesText`() {
        // 1. Arrange (Find the views)
        val buttonMatcher = withId(R.id.my_button)
        val textMatcher = withId(R.id.my_text_view)

        // 2. Act (Perform a click)
        onView(buttonMatcher).perform(click())

        // 3. Assert (Check if the text changed)
        onView(textMatcher).check(matches(withText("You clicked the button!")))
    }
}

Option B: Jetpack Compose Test (The Modern Way)

Testing with Compose is *much* easier and more integrated. You don't need to worry about ActivityScenarioRule.

Step 1: Add Compose Test Dependencies

dependencies {
    // ...
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-tooling")
}

Step 2: Write the Compose Test

We can test our MyCounterScreen Composable directly, without even needing an Activity!

// In src/androidTest/java/.../MyCounterScreenTest.kt
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule
import org.junit.Test

class MyCounterScreenTest {
    
    // 1. Create a "Compose Test Rule"
    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun `clickButton_incrementsCounter`() {
        // 1. Arrange: Set the Composable we want to test
        composeTestRule.setContent {
            MyCounterScreen() // This uses a real ViewModel
        }
        
        // 2. Act: Find the button and click it
        // We use 'onNode' (like Espresso's 'onView')
        composeTestRule
            .onNodeWithText("Click Me")
            .performClick()

        // 3. Assert: Check if the text updated
        composeTestRule
            .onNodeWithText("You clicked 1 times")
            .assertIsDisplayed()
            
        // Act again
        composeTestRule.onNodeWithText("Click Me").performClick()
        
        // Assert again
        composeTestRule.onNodeWithText("You clicked 2 times").assertIsDisplayed()
    }
}

Final Step: Testing with Hilt (DI)

This is the most advanced and most professional technique. How do you UI-test a screen that injects a *real* ViewModel, which injects a *real* Repository, which makes a *real* network call? Your test will fail if the internet is down.

You need to **replace your dependencies** at test-time. Hilt lets you do this easily.

Step 1: Add Hilt Testing Dependencies

dependencies {
    // ...
    kspAndroidTest("com.google.dagger:hilt-compiler:2.48")
    androidTestImplementation("com.google.dagger:hilt-android-testing:2.48")
}

Step 2: Create a Fake Repository (in `src/androidTest`)

Create a "fake" repository in your `androidTest` folder that does *not* make network calls.

Step 3: Create a Fake Hilt Module (in `src/androidTest`)

Create a Hilt module in your `androidTest` folder that tells Hilt: "When you run a test, **UNINSTALL** the real AppModule and **INSTALL** this *fake* module instead."

// In src/androidTest/java/.../di/TestAppModule.kt
@Module
@InstallIn(SingletonComponent::class)
// This tells Hilt to *uninstall* the AppModule we defined in the main app
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [AppModule::class]
)
object TestAppModule {
    
    // This provider *overrides* the real one
    @Provides
    @Singleton
    fun provideFakeNotesRepository(): NotesRepository {
        return FakeNotesRepository() // Return our fake one!
    }
    
    // (Hilt will still provide the real Dao, ApiService, etc. if you don't override them)
}

Step 4: Write the Hilt UI Test

// In src/androidTest/java/.../MyScreenHiltTest.kt
@HiltAndroidTest // 1. Add this annotation
@RunWith(AndroidJUnit4::class)
class MyScreenHiltTest {

    // 2. Add Hilt rule (must run before Compose rule)
    @get:Rule(order = 0)
    val hiltRule = HiltAndroidRule(this)

    @get:Rule(order = 1)
    val composeTestRule = createComposeRule()

    @Test
    fun `screen_launches_and_uses_fake_data`() {
        // 3. Hilt automatically injects fakes
        hiltRule.inject() 
        
        composeTestRule.setContent {
            // This NotesViewModel will now be created by Hilt,
            // and Hilt will give it the FakeNotesRepository!
            MyScreen(viewModel = hiltViewModel())
        }
        
        // 4. Assert against the fake data
        composeTestRule
            .onNodeWithText("Fake Note 1") // (Assuming FakeNotesRepository provides this)
            .assertIsDisplayed()
    }
}

This is the absolute standard for professional, testable, and robust Android applications.

Read the Official Guide to Testing →