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.
TDD is either praised as the only correct way to develop or dismissed as impractical. The reality is more nuanced. Here's how to apply TDD where it adds value in Android development without dogma.
On this page
TDD has two camps: the true believers who write tests for everything before a single line of production code, and the pragmatists who see it as too slow for "real" development. Both are wrong in interesting ways.
Here's the honest picture.
The TDD loop:
The key insight: you're using the test as a design tool, not just a verification tool. Writing the test first forces you to think about the interface before the implementation.
Business logic with clear rules. Tax calculations, discount rules, validation logic, state machines. The rules are precise and testable. TDD is natural here.
// TDD for a discount rule:
// Step 1: Write the test (Red)
@Test
fun `order over 100 gets 10 percent discount`() {
val order = Order(total = 120.0)
val discount = discountService.calculate(order)
assertEquals(12.0, discount, 0.001)
}
// Step 2: Write minimum code (Green)
fun calculate(order: Order): Double {
return if (order.total > 100) order.total * 0.10 else 0.0
}
// Step 3: Add edge cases (more Red)
@Test
fun `order exactly 100 does not get discount`() {
val order = Order(total = 100.0)
assertEquals(0.0, discountService.calculate(order), 0.001)
}Data transformations and mappers. Mapping network DTOs to domain models is pure function territory. TDD fits perfectly.
Algorithms. Sorting, parsing, complex calculations. The expected output is knowable before you write the algorithm.
State management in ViewModels. Given this action, the state should be X. Very testable with Turbine.
UI development. You can't easily TDD a composable layout. You'd have to run the app to see what you're building. Write the composable first, add a screenshot test after.
Exploratory code. When you're not sure what you're building yet, tests lock you into a design you might discard. Explore first, then extract testable logic and TDD that.
External integrations. Figuring out how a third-party SDK works requires experimentation. TDD doesn't help you discover an API — it helps you specify behavior you already understand.
Database schema changes. Room migrations are hard to TDD — you learn the constraints by running them. Write migration tests after the migration is defined.
[!NOTE] TDD is a design technique, not a testing technique. It works best when you already understand the design you're implementing. When you're figuring out the design, explore first.
// Feature: Filter tasks by status
// Step 1: Red — test behavior we want
@Test
fun `filtering by active shows only incomplete tasks`() = runTest {
val repository = FakeTaskRepository().apply {
addTask(Task("1", "Buy milk", completed = false))
addTask(Task("2", "Write tests", completed = true))
addTask(Task("3", "Deploy app", completed = false))
}
val viewModel = TaskViewModel(repository)
viewModel.setFilter(TaskFilter.ACTIVE)
val state = viewModel.uiState.value
assertEquals(2, state.tasks.size)
assertTrue(state.tasks.all { !it.completed })
}
// Step 2: Green — add filter support to ViewModel
class TaskViewModel(private val repository: TaskRepository) : ViewModel() {
private val _filter = MutableStateFlow(TaskFilter.ALL)
val uiState: StateFlow<TaskUiState> = combine(
repository.getAllTasks(),
_filter
) { tasks, filter ->
TaskUiState(
tasks = when (filter) {
TaskFilter.ALL -> tasks
TaskFilter.ACTIVE -> tasks.filter { !it.completed }
TaskFilter.COMPLETED -> tasks.filter { it.completed }
}
)
}.stateIn(viewModelScope, SharingStarted.Eagerly, TaskUiState())
fun setFilter(filter: TaskFilter) {
_filter.value = filter
}
}
// Step 3: Add more cases (Red → Green → Refactor)
@Test
fun `filtering by completed shows only completed tasks`() = runTest { ... }
@Test
fun `filter all shows all tasks`() = runTest { ... }For code you write without TDD:
This isn't TDD, but it achieves most of the same goals if you do it immediately — not "later."
"Later" always becomes "never."
When starting a new feature:
Identify the pure logic. Which parts of this feature are business rules or data transformations? Those are TDD candidates.
Identify the exploratory parts. Which parts require figuring out an API, UI layout, or architecture? Do those first.
TDD the logic. Once you understand what you're building, write tests first for the business rules.
Test after for the plumbing. Repository implementations, DB migration tests, UI tests — write after, immediately.
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