Skip to content
All posts
May 27, 20264 min read

Writing Testable Android Code: ViewModels, Fakes, and Coroutine Test Rules

This post covers practical strategies for writing testable Android code using ViewModels, fakes, and coroutine test rules to improve maintainability and reduce flaky tests.

AndroidKotlinTestingCoroutines
Share:

Hook

Testable code isn’t a luxury—it’s a survival tool for solo Android developers. When your app grows beyond 10,000 lines, flaky tests and brittle integrations become your worst enemy. I’ve spent years debugging tests that failed randomly, and the solution? A structured approach to testability. Today, I’ll share how ViewModels, fakes, and coroutine test rules transformed my workflow at SudarshanTechLabs.

Context

As a solo developer building 22+ apps, I’ve learned that testability is about reducing coupling and making assumptions explicit. Android’s default patterns often encourage tight coupling between UI, business logic, and data layers. This makes testing hard. ViewModels are great for separating UI from logic, but they’re not inherently testable. Fakes (mocks, stubs) help isolate components, but using them poorly leads to test fragility. Coroutines add complexity because their asynchronous nature can break test assumptions. This post tackles all three.

1. ViewModels: Separate Logic, Enable Testing

ViewModels are central to Android’s MVVM pattern, but they’re often misused. Many devs tie ViewModel logic to UI callbacks, making tests brittle. The key is to decouple ViewModel dependencies from UI code.

Why ViewModels Matter for Testing

A well-structured ViewModel:

  • Contains business logic, not UI-specific code
  • Accepts dependencies via constructor injection
  • Emits state via
    code
    StateFlow
    or
    code
    LiveData

Example: Testable ViewModel

kotlin
// Bad: UI callbacks in ViewModel  
class BadViewModel(private val repo: Repo) {  
    fun loadData() {  
        repo.fetchData { data ->  
            // Update UI directly here  
        }  
    }  
}  

// Good: Decoupled ViewModel  
class GoodViewModel(private val repo: Repo) {  
    val dataFlow = Flow.create()  

    fun loadData() {  
        repo.fetchData { data ->  
            dataFlow.emit(data)  
        }  
    }  
}  

Code Block: ViewModel with Dependency Injection

kotlin
// Using Hilt for injection  
@HiltViewModel  
class MyViewModel @Inject constructor(private val repo: MyRepo) {  
    val dataFlow = Flow.create()  

    fun loadData() {  
        repo.fetchData { data ->  
            dataFlow.emit(data)  
        }  
    }  
}  

[!TIP]

Inject dependencies via constructor. Never create repositories or data sources inside ViewModels. This makes mocking easier.

2. Fakes: Isolate Components, Avoid Flakiness

Fakes (mocks, stubs) are essential for testing, but they’re often overused or misconfigured. The goal is to mock only what’s necessary, not the entire app.

Types of Fakes

Fake TypeWhen to UseExample
MockVerify interactionsMock a repository’s API call
StubReturn fixed valuesStub a network response
SpyTrack method callsSpy on a ViewModel’s function

Example: Mocking a Repository

kotlin
// Real repository  
interface Repo {  
    suspend fun getUser(id: Int): User  
}  

// Mock repository for tests  
class MockRepo : Repo {  
    private val user = User(id = 1, name = "Test")  

    override suspend fun getUser(id: Int) = user  
}  

Code Block: Using MockK for Testing

kotlin
// Test using MockK  
class MyViewModelTest {  
    private val mockRepo = mockk<Repo>()  

    @Test  
    fun testLoadData() {  
        every { mockRepo.getUser(any()) } returns User(id = 1, name = "Test")  
        val viewModel = MyViewModel(mockRepo)  
        viewModel.loadData()  
        verify { mockRepo.getUser(1) }  
    }  
}  

[!WARNING]

Avoid mocking too much. If you mock a repository, you lose real data validation. Use fakes only for unstable or external dependencies.

3. Coroutine Test Rules: Avoid Asynchronous Gotchas

Coroutines are powerful, but they introduce challenges in testing. Tests must respect the same concurrency rules as the app.

Common Coroutine Testing Mistakes

  • Forgetting to
    code
    await
    suspend functions
  • Running tests on a non-coroutine thread
  • Not using
    code
    runTest
    for coroutine tests

Coroutine Test Rules

  1. Use
    code
    runTest
    for coroutine tests
    : This ensures tests run on a coroutine context.
  2. Avoid
    code
    await
    in tests
    : Use
    code
    await
    only in suspend functions. Tests should be imperative.
  3. Mock coroutine-related dependencies: Mock delays or background threads if needed.

Example: Coroutine Test Setup

kotlin
// Correct test using runTest  
@Test  
fun testCoroutine() = runTest {  
    val result = withContext(Dispatchers.IO) {  
        // Simulate async work  
        delay(100)  
        "Success"  
    }  
    assertEquals("Success", result)  
}  

Code Block: Coroutine Test with Mock Delay

kotlin
// Mock a delay in tests  
class MyServiceTest {  
    private val mockDelay = mockk<Delay>()  

    @Test  
    fun testWithMockDelay() {  
        every { mockDelay.delay(any()) } returns Unit  
        val service = MyService(mockDelay)  
        val result = service.doWork()  
        assertEquals("Done", result)  
    }  
}  

[!NOTE]

For coroutine tests, prefer

code
runTest
over
code
assertEquals
with
code
await
. This makes tests faster and more reliable.

Key Takeaways

  • Inject dependencies into ViewModels to enable easy mocking and testing.
  • Use fakes selectively—mock only unstable or external dependencies.
  • Apply coroutine test rules like
    code
    runTest
    and avoid
    code
    await
    in test logic.

These practices don’t just make tests pass—they make your codebase maintainable. At SudarshanTechLabs, I’ve seen flaky tests drop by 70% after applying these rules. Start small: pick one ViewModel or coroutine test and refactor it today.

Share:
S

Sudarshan Chaudhari

AI Systems Builder / Product Engineer

Bangkok, Thailand

Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

MyFamilyTracker

Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus

Apps tagged with this