The Red-Green-Refactor Cycle
The Red-Green-Refactor Cycle
TDD is a development discipline, not a testing technique. You write a failing test first, make it pass with the minimum code, then clean up.
The Three Steps
1. Red — Write a Failing Test
Write a test that describes the behavior you want. It must fail because the code does not exist yet.
@Test
fun `cart total updates when item added`() = runTest {
val cart = ShoppingCart()
cart.addItem(Product(id = "1", price = 9.99))
assertThat(cart.total).isEqualTo(9.99)
}
This fails because ShoppingCart does not exist. Good.
2. Green — Make It Pass
Write the simplest code that makes the test pass. No more.
class ShoppingCart {
val total: Double
get() = items.sumOf { it.price }
private val items = mutableListOf<Product>()
fun addItem(product: Product) {
items.add(product)
}
}
The test passes. The code is ugly but correct.
3. Refactor — Clean Up
Now improve the code without changing behavior:
class ShoppingCart {
private val items = mutableListOf<Product>()
val total: Double
get() = items.sumOf { it.price }
val itemCount: Int
get() = items.size
fun addItem(product: Product) {
items.add(product)
}
fun removeItem(productId: String) {
items.removeAll { it.id == productId }
}
fun clear() {
items.clear()
}
}
Add more tests for the new methods. Repeat.
Why This Order Matters
- Test-first forces you to think about the interface before the implementation.
- Minimum code prevents gold-plating and over-engineering.
- Refactor happens when you have tests to catch regressions.
TDD on Android: Practical Guide
TDD on Android: Practical Guide
Android has unique constraints that affect TDD. Activities depend on the framework, ViewModels depend on Dispatchers.Main, and UI depends on Compose or Views.
TDD for ViewModels
ViewModels are the easiest place to start TDD because they are plain Kotlin classes:
// Step 1: Write the failing test
@Test
fun `loadUser shows loading then success`() = runTest {
coEvery { userRepository.getUser("1") } returns User("Alice")
val viewModel = UserViewModel(userRepository)
viewModel.loadUser("1")
val state = viewModel.uiState.first()
assertThat(state).isInstanceOf(UserUiState.Success::class.java)
assertThat((state as UserUiState.Success).user.name).isEqualTo("Alice")
}
// Fails: UserViewModel does not exist
// Step 2: Implement minimal code to pass
class UserViewModel(private val repo: UserRepository) : ViewModel() {
private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
val uiState: StateFlow<UserUiState> = _uiState
fun loadUser(id: String) {
viewModelScope.launch {
_uiState.value = UserUiState.Loading
try {
val user = repo.getUser(id)
_uiState.value = UserUiState.Success(user)
} catch (e: Exception) {
_uiState.value = UserUiState.Error(e.message ?: "Unknown error")
}
}
}
}
// Step 3: Refactor — extract error message, add test for error path
TDD for Repositories
// Step 1: Failing test
@Test
fun `getProducts returns mapped list`() = runTest {
val repository = ProductRepository(dao)
dao.insertAll(listOf(ProductEntity(id = "1", name = "Widget")))
val products = repository.getProducts()
assertThat(products).hasSize(1)
assertThat(products[0].name).isEqualTo("Widget")
}
// Fails: getProducts does not map correctly
// Step 2: Make it pass
suspend fun getProducts(): List<Product> {
return dao.getAll().map { it.toDomain() }
}
// Step 3: Refactor — add caching, error handling
TDD for Compose UI
// Step 1: Write a failing Compose test
@Test
fun loginScreen_showsErrorForEmptyEmail() {
composeTestRule.setContent {
MaterialTheme { LoginScreen() }
}
composeTestRule.onNodeWithTag("login_button").performClick()
composeTestRule.onNodeWithTag("email_error")
.assertIsDisplayed()
}
// Fails: error is not shown
// Step 2: Add validation to the composable
@Composable
fun LoginScreen(viewModel: LoginViewModel = hiltViewModel()) {
var email by remember { mutableStateOf("") }
var showEmailError by remember { mutableStateOf(false) }
Column {
TextField(
value = email,
onValueChange = { email = it },
modifier = Modifier.testTag("email_input")
)
if (showEmailError) {
Text("Email is required", modifier = Modifier.testTag("email_error"))
}
Button(
onClick = { showEmailError = email.isBlank() },
modifier = Modifier.testTag("login_button")
) {
Text("Login")
}
}
}
Android-Specific TDD Tips
- Start with the ViewModel. It is pure Kotlin and easiest to test.
- Use Hilt for dependency injection. It makes swapping real and fake dependencies trivial.
- Write one test at a time. Resist the urge to write multiple failing tests.
- Keep tests small. Each test should verify one behavior.
- Refactor after every green. Do not let debt accumulate.
- Use the testing pyramid. TDD drives unit tests. Add integration and UI tests later for critical paths.
Quiz
1. What is the correct order of the TDD cycle?
2. In TDD, what should the green step produce?
3. Why is TDD particularly effective for ViewModels on Android?
4. When should you add integration or UI tests in a TDD workflow?
Flashcards
Question
What are the three steps of the TDD cycle?
Click to reveal answer
Answer
Red: write a failing test. Green: make it pass with minimum code. Refactor: clean up while tests protect against regressions.
Question
What is the purpose of the refactor step in TDD?
Click to reveal answer
Answer
To improve code structure, readability, and remove duplication without changing behavior, protected by the tests you already wrote.
Question
Where should you start TDD on an Android project?
Click to reveal answer
Answer
ViewModels — they are pure Kotlin classes with no framework dependencies, making them the easiest place to apply TDD.
Question
What is the danger of writing too many failing tests at once in TDD?
Click to reveal answer
Answer
You lose focus on the specific behavior and may implement more than necessary. Write one test, make it pass, then move to the next.
Revision Notes
Key Takeaways
- 1. TDD is Red-Green-Refactor: failing test, minimum code to pass, clean up
- 2. Start TDD with ViewModels because they are pure Kotlin classes
- 3. Write the minimum code to pass — do not over-engineer in the green step
- 4. Refactor confidently because tests catch regressions
- 5. Add integration and UI tests after unit tests for critical paths
Interview Tips
- • Walk through a Red-Green-Refactor cycle with a concrete Android example
- • Explain why test-first design leads to better interfaces
- • Discuss how TDD helps with refactoring legacy Android code
- • Know the difference between TDD and writing tests after implementation
Cheat Sheet
TDD on Android Cheat Sheet
Red-Green-Refactor:
- Red: Write a failing test that describes desired behavior
- Green: Write minimum code to make the test pass
- Refactor: Clean up code while tests pass
Android TDD Order:
- ViewModel (pure Kotlin, no framework)
- Repository (uses fakes/in-memory DB)
- Use cases (business logic)
- Compose UI (testTag, assertions)
TDD Rules:
- Never write production code without a failing test
- Write the minimum code to pass
- Refactor after every green step
- One test at a time
- Tests are your safety net for refactoring
Common Patterns:
- Start with happy path, add error paths after
- Use fake dependencies from the start
- Keep tests small and focused on one behavior