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.
This post covers practical strategies for writing testable Android code using ViewModels, fakes, and coroutine test rules to improve maintainability and reduce flaky tests.
On this page
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.
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.
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.
A well-structured ViewModel:
StateFlowLiveData// 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)
}
}
} // 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)
}
}
} Inject dependencies via constructor. Never create repositories or data sources inside ViewModels. This makes mocking easier.
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.
| Fake Type | When to Use | Example |
|---|---|---|
| Mock | Verify interactions | Mock a repository’s API call |
| Stub | Return fixed values | Stub a network response |
| Spy | Track method calls | Spy on a ViewModel’s function |
// 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
} // 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) }
}
} Avoid mocking too much. If you mock a repository, you lose real data validation. Use fakes only for unstable or external dependencies.
Coroutines are powerful, but they introduce challenges in testing. Tests must respect the same concurrency rules as the app.
awaitrunTestrunTestawaitawait// Correct test using runTest
@Test
fun testCoroutine() = runTest {
val result = withContext(Dispatchers.IO) {
// Simulate async work
delay(100)
"Success"
}
assertEquals("Success", result)
} // 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)
}
} For coroutine tests, prefer
runTestassertEqualsawaitrunTestawaitThese 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.
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.
Related Posts
Related Apps
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 meComments — powered by Giscus
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
ReadPrivate dream journal — structured entry capture, pattern tagging, and optional Claude-powered insight generation. All data stays on-device by default.
ReadWorkout tracker — exercise logging with set/rep/weight history, goal progression, and local Room DB persistence. No account, no cloud sync required.
Read