AgentStack
SKILL verified MIT Self-run

Test Driven Development

skill-guillemroca-agent-skills-android-test-driven-development · by GuillemRoca

>-

No reviews yet
0 installs
0 views
view→install

Install

$ agentstack add skill-guillemroca-agent-skills-android-test-driven-development

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No 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.

Are you the author of Test Driven Development? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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
  1. 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)
    }
}
  1. 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
  1. 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)
}
  1. Run the test — it must pass
  2. Don't add code "you'll need later" — only what the test demands
REFACTOR: Improve Without Changing Behavior
  1. Clean up while tests stay green:
  • Extract common patterns
  • Improve naming
  • Reduce duplication
  • Run tests after every change

The Prove-It Pattern (Bug Fixes)

  1. 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

  1. 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...
    }
}
  1. 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" }) }
    }
}
  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 with TestDispatcher
  • Use advanceUntilIdle() — not delay() or Thread.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.sleep or hardcoded delays in tests
  • Tests that depend on execution order
  • Mocking internal classes (mock boundaries only)
  • No tests for error/edge cases
  • @Ignore or @Disabled tests 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 test passes (all unit tests)
  • [ ] Test names describe behavior, not implementation
  • [ ] No Thread.sleep in tests (use advanceUntilIdle)
  • [ ] 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.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.