Install
$ agentstack add skill-guillemroca-agent-skills-android-test-driven-development ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Test-Driven Development
Overview
Write tests before implementation. The Red-Green-Refactor cycle — write a failing test (RED), write minimal code to pass (GREEN), improve the code without changing behavior (REFACTOR) — produces code that is correct by construction and has comprehensive test coverage from the start.
When to Use
- Implementing any new feature or behavior
- Fixing any bug (use the Prove-It pattern)
- Adding new business logic to ViewModels, use cases, or repositories
- Before refactoring (establish a test safety net first)
Skip when: Purely cosmetic UI changes with no behavioral logic.
Core Process
Red-Green-Refactor Cycle
RED: Write a Failing Test
- Write the test first — describe what the code should do:
class TaskListViewModelTest {
private val repository = mockk()
private lateinit var viewModel: TaskListViewModel
@Test
fun `initial state is Loading`() {
every { repository.getTasks() } returns flowOf(emptyList())
viewModel = TaskListViewModel(GetTasksUseCase(repository))
assertEquals(TaskListUiState.Loading, viewModel.uiState.value)
}
@Test
fun `emits Success with tasks when repository returns data`() = runTest {
val tasks = listOf(Task("1", "Buy groceries", false))
every { repository.getTasks() } returns flowOf(tasks)
viewModel = TaskListViewModel(GetTasksUseCase(repository))
advanceUntilIdle()
assertEquals(TaskListUiState.Success(tasks), viewModel.uiState.value)
}
@Test
fun `emits Error when repository throws`() = runTest {
every { repository.getTasks() } returns flow { throw IOException("Network error") }
viewModel = TaskListViewModel(GetTasksUseCase(repository))
advanceUntilIdle()
val state = viewModel.uiState.value
assertIs(state)
assertEquals("Network error", state.message)
}
}
- Run the test — it must fail (
./gradlew test)
- If it passes immediately, the test doesn't test what you think
GREEN: Write Minimal Code to Pass
- Implement just enough to make the test pass:
@HiltViewModel
class TaskListViewModel @Inject constructor(
getTasks: GetTasksUseCase,
) : ViewModel() {
val uiState: StateFlow = getTasks()
.map, TaskListUiState> { TaskListUiState.Success(it) }
.catch { emit(TaskListUiState.Error(it.message ?: "Unknown error")) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), TaskListUiState.Loading)
}
- Run the test — it must pass
- Don't add code "you'll need later" — only what the test demands
REFACTOR: Improve Without Changing Behavior
- Clean up while tests stay green:
- Extract common patterns
- Improve naming
- Reduce duplication
- Run tests after every change
The Prove-It Pattern (Bug Fixes)
- For every bug, prove it exists before fixing:
// Step 1: Write a test that demonstrates the bug
@Test
fun `completed tasks should not appear in active filter`() {
val tasks = listOf(
Task("1", "Active task", completed = false),
Task("2", "Done task", completed = true),
)
every { repository.getTasks() } returns flowOf(tasks)
viewModel = TaskListViewModel(GetTasksUseCase(repository))
viewModel.setFilter(TaskFilter.ACTIVE)
advanceUntilIdle()
val state = viewModel.uiState.value as TaskListUiState.Success
assertEquals(1, state.tasks.size)
assertEquals("Active task", state.tasks[0].title)
}
// Step 2: Run it — confirm it FAILS (proves the bug exists)
// Step 3: Fix the code
// Step 4: Run it — confirm it PASSES (proves the fix works)
// Step 5: Run full suite — no regressions
Test Pyramid
/‾‾‾‾‾‾‾‾‾\
/ UI Tests \ ~5% — Compose rules, Espresso, Maestro flows
/ (device) \ see: android-device-testing, android-e2e-verification
/‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\
/ Integration Tests \ ~15% — Room in-memory DB, MockWebServer,
| (local/device) | Robolectric
|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|
| Unit Tests | ~80% — JUnit5 + MockK
| (fast, local) | ViewModels, UseCases, Repositories
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
Testing Patterns by Layer
- ViewModel tests:
@OptIn(ExperimentalCoroutinesApi::class)
class ViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule() // Sets Dispatchers.Main to TestDispatcher
@Test
fun `action updates state correctly`() = runTest {
// Arrange
val viewModel = createViewModel()
// Act
viewModel.onEvent(TaskListEvent.ToggleTask("1"))
advanceUntilIdle()
// Assert
val state = viewModel.uiState.value
// ...assertions...
}
}
- Repository tests (integration):
class TaskRepositoryTest {
private val dao = mockk()
private val api = mockk()
private val mapper = TaskMapper()
private val repository = TaskRepositoryImpl(dao, api, mapper)
@Test
fun `syncTasks fetches from API and saves to database`() = runTest {
val apiTasks = listOf(TaskResponse("1", "Test"))
coEvery { api.getTasks() } returns apiTasks
coEvery { dao.upsertAll(any()) } just Runs
repository.syncTasks()
coVerify { dao.upsertAll(match { it.size == 1 && it[0].id == "1" }) }
}
}
- Room DAO tests:
@RunWith(AndroidJUnit4::class)
class TaskDaoTest {
private lateinit var db: AppDatabase
private lateinit var dao: TaskDao
@Before
fun setup() {
db = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase::class.java,
).build()
dao = db.taskDao()
}
@After
fun teardown() { db.close() }
@Test
fun upsert_and_observe_returns_task() = runTest {
val task = TaskEntity("1", "Test", null, false)
dao.upsert(task)
val result = dao.observeAll().first()
assertEquals(1, result.size)
assertEquals("Test", result[0].title)
}
}
Best Practices
- State over interactions — assert on outcomes, not method calls
- DAMP over DRY — each test should be self-contained and readable
- Prefer real > fakes > stubs > mocks — mock only at system boundaries
- Descriptive names — test names read like behavior specifications
- Use
runTest— for coroutine testing withTestDispatcher - Use
advanceUntilIdle()— notdelay()orThread.sleep()
Common Rationalizations
| Shortcut | Why It Fails | |----------|-------------| | "I'll write tests after the code works" | You won't. And tests written after are weaker — they verify what you built, not what you intended. | | "TDD is slow" | TDD prevents debugging. Net time is less. | | "This is too simple to test" | Simple code becomes complex code. Tests catch the transition. | | "Mocking is too complex for this" | If it's hard to mock, the design has too many dependencies. TDD surfaces this. | | "The test passed on the first try, so it's good" | A test that never failed may not test what you think. Make it fail first. |
Red Flags
- Code written before tests
- Tests that pass immediately (never saw RED)
Thread.sleepor hardcoded delays in tests- Tests that depend on execution order
- Mocking internal classes (mock boundaries only)
- No tests for error/edge cases
@Ignoreor@Disabledtests without issue references
Verification
- [ ] Tests written BEFORE implementation (Red-Green-Refactor)
- [ ] Every test seen failing before passing
- [ ] Bug fixes use the Prove-It pattern
- [ ]
./gradlew testpasses (all unit tests) - [ ] Test names describe behavior, not implementation
- [ ] No
Thread.sleepin tests (useadvanceUntilIdle) - [ ] Test pyramid proportions reasonable (~80/15/5)
- [ ] Edge cases and error paths covered
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: GuillemRoca
- Source: GuillemRoca/agent-skills-android
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.