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.
Full test coverage is a fantasy for solo developers. A targeted strategy that covers what matters — ViewModel logic, repository contracts, and critical UI flows — without drowning in test maintenance.
On this page
Testing is one area where solo developer advice diverges sharply from team advice. "Aim for 80% coverage" is correct for a team with dedicated QA engineers. For a solo developer shipping across 22+ apps, it's not a realistic target and it's not the right goal.
Here's a testing strategy that's actually sustainable.
Not everything deserves a test. The goal is to catch the bugs that would cost you the most time or embarrass you the most publicly.
Always test:
Test selectively:
Don't bother:
This is the highest-value test category. Your ViewModel contains your app's decision-making logic.
@OptIn(ExperimentalCoroutinesApi::class)
class LoginViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
private val fakeRepository = FakeAuthRepository()
private lateinit var viewModel: LoginViewModel
@Before
fun setup() {
viewModel = LoginViewModel(fakeRepository)
}
@Test
fun `valid credentials transition to Success state`() = runTest {
fakeRepository.setLoginResult(Result.success(fakeUser))
viewModel.onLoginClicked("user@test.com", "password")
advanceUntilIdle()
assertEquals(LoginUiState.Success(fakeUser), viewModel.uiState.value)
}
@Test
fun `network error shows error state`() = runTest {
fakeRepository.setLoginResult(Result.failure(IOException("no connection")))
viewModel.onLoginClicked("user@test.com", "password")
advanceUntilIdle()
assertTrue(viewModel.uiState.value is LoginUiState.Error)
}
}Use fake implementations, not mocks. A
FakeAuthRepositoryverify()You need this for every ViewModel test:
class MainDispatcherRule(
private val dispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}Without it,
viewModelScopeTest the Repository's contract — not the implementation details:
class UserRepositoryTest {
private val fakeApi = FakeUserApi()
private val fakeDao = FakeUserDao()
private val repository = UserRepository(fakeApi, fakeDao)
@Test
fun `getUser returns cached value when available`() = runTest {
fakeDao.insert(cachedUser)
val result = repository.getUser("user_id")
assertEquals(cachedUser, result.getOrNull())
assertEquals(0, fakeApi.callCount) // API not called
}
@Test
fun `getUser fetches from API when cache is empty`() = runTest {
fakeApi.setResponse("user_id", remoteUser)
val result = repository.getUser("user_id")
assertEquals(remoteUser, result.getOrNull())
assertEquals(1, fakeApi.callCount)
}
}Compose UI tests are slow and brittle. Write them for:
@Test
fun loginScreen_displaysErrorOnInvalidCredentials() {
composeTestRule.setContent {
LoginScreen(
uiState = LoginUiState.Error("Invalid credentials"),
onLoginClicked = {}
)
}
composeTestRule
.onNodeWithText("Invalid credentials")
.assertIsDisplayed()
}Pass state directly into Composables rather than testing through a ViewModel — it's faster and more reliable.
This gives you meaningful coverage without a test suite that takes 20 minutes to run and breaks every time you rename a variable.
The goal isn't a percentage. It's catching the bugs that matter before your users do.
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