Testing
๐งช Testing
โก Unit Tests โ Fast, No Device Required
Unit tests live in src/test/ and run directly on the local JVM โ no emulator, no real device, seconds instead of minutes. They're the right tool for anything that's pure logic: a repository, a use case, a ViewModel, none of which genuinely need actual Android framework code to test.
This is exactly why Course 2 Chapter 8's interface-plus-@Binds pattern mattered: NoteViewModel's constructor only ever demanded a NoteRepository, never NoteRepositoryImpl specifically โ so a test can hand it this FakeNoteRepository instead, with zero Room, zero Retrofit, zero network or database access anywhere in the test.
runTest (from kotlinx-coroutines-test) provides a special coroutine scope built for testing โ it runs suspending code in a controlled, virtual-time environment, so delay(2000) anywhere in the code under test doesn't actually make the test wait 2 real seconds. Kotlin function names can be arbitrary strings inside backticks (a Kotlin syntax feature, not test-specific) โ a common convention for making test names read as full sentences.
๐ฑ Instrumented Tests โ Need a Real (or Virtual) Device
Instrumented tests live in src/androidTest/ and run on an actual Android environment โ an emulator or physical device โ because they exercise real framework behavior a plain JVM can't fake: launching a real Activity, real Views, actual touch/click dispatch.
Unit Tests vs Instrumented Tests
| Unit Tests (src/test/) | Instrumented Tests (src/androidTest/) | |
|---|---|---|
| Runs on | Local JVM | Emulator or real device |
| Speed | Fast โ seconds | Slow โ needs a booted device |
| Good for | ViewModels, repositories, pure logic | Real UI interaction, Activity lifecycle, actual rendering |
| Android framework access | None (or a limited fake via Robolectric) | Full, real |
๐ Espresso โ Testing the View System (Course 1)
Espresso drives and asserts against real Views, for apps (or screens) still using Course 1's XML/View system:
onView(withId(...)) locates a real View by its resource ID (Course 1, Chapter 3), .perform(click()) genuinely dispatches a click, and .check(matches(...)) asserts on the resulting state โ the same click-then-verify pattern behind every UI test, regardless of framework.
๐๏ธ Compose UI Testing โ The Modern Equivalent
For Compose screens (Course 2 onward), a parallel API replaces Espresso's View-based lookups with node-based ones:
Espresso vs Compose Testing
| Espresso | Compose Testing | |
|---|---|---|
| Finding an element | onView(withId(R.id.x)) | onNodeWithText(...) / onNodeWithTag(...) |
| Action | .perform(click()) | .performClick() |
| Assertion | .check(matches(withText(...))) | .assertTextEquals(...) / .assertIsDisplayed() |
| Setup | ActivityScenario.launch(...) | composeTestRule.setContent { ... } |
onNodeWithTag(...) (paired with a Modifier.testTag("...") on the composable itself) is the more robust lookup when text alone isn't unique or reliable enough โ the Compose-testing equivalent of Espresso's ID-based lookup, since Compose has no R.id resource system to key off of the way the View system does.
Android Testing vs JavaScript Testing
| JavaScript (Jest / Testing Library) | Android | |
|---|---|---|
| Pure logic tests | Jest unit tests | JUnit unit tests (src/test/) |
| UI interaction tests | Testing Library (render, screen.getByText, fireEvent) | Compose UI tests (onNodeWithText, performClick) |
| Faking dependencies | jest.mock(...) | A fake implementation of an interface (this chapter's FakeNoteRepository) |
๐ป Coding Challenges
Challenge 1: A Fake Repository and ViewModel Unit Test
Write a FakeTaskRepository implementing Course 2's TaskRepository interface with a controllable in-memory list, then a unit test for TaskViewModel confirming that calling addTask("Buy milk") results in that task appearing in uiState.value.tasks, using runTest and advanceUntilIdle().
Goal: Practice the fake-dependency unit-testing pattern for a ViewModel.
Challenge 2: A Compose UI Test
Write a Compose UI test for Course 2's CounterScreen composable, using createComposeRule() to set its content, performing two clicks on the Increment button, and asserting the displayed count text reads "Count: 2".
Goal: Practice the full Compose testing setup: rule, setContent, node lookup, action, assertion.
Challenge 3: Choosing the Right Test Type
For each of the following, state whether a unit test or an instrumented/Compose UI test is appropriate, with a one-sentence reason: (a) NoteRepository's refresh() logic with a fake API, (b) confirming a Button's click actually navigates to a new screen, (c) a pure Kotlin function calculating tax on a price, (d) confirming a TextView's text updates after a ViewModel state change.
Goal: Practice the judgment call between test types, not just the syntax of either.
Every "why does this ViewModel take an interface instead of a concrete class" moment from Course 2 pays off directly in this chapter โ a codebase built around concrete classes and singletons everywhere would make this chapter's fakes far harder, or impossible, to write cleanly. Good architecture and testability aren't separate concerns; they're the same decisions, viewed from two angles.
๐ฏ What's Next
Next chapter: Background Work โ WorkManager, foreground services, and scheduling tasks.