agentsclimarketplace

Mobiai android testing

Skill ArisGuimera/MobiAI-Core/skills/android/skills/mobiai-android-testing

Use when writing or running tests in an Android project — unit tests, UI tests, choosing the right framework and patterns.From its SKILL.md

Install
npx -y skills add ArisGuimera/MobiAI-Core --skill mobiai-android-testing

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its file declares

Copied from the file, not written here

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

6.4 KB, as published. Nobody here has run it

Android Testing

Comprehensive guide to writing and running tests in Android projects.

When to Use

  • Writing unit tests for Android/Kotlin code
  • Setting up test infrastructure
  • Running and debugging test suites
  • Choosing the right testing approach

Test Types

TypeLocationFrameworkSpeedUses Device
Unitsrc/test/JUnit + MockKFastNo
Integrationsrc/test/JUnit + RobolectricMediumNo (simulated)
UI / Instrumentedsrc/androidTest/Espresso / Compose TestingSlowYes

Unit Testing (JUnit + MockK)

Setup

// build.gradle.kts
dependencies {
    testImplementation("junit:junit:4.13.2")
    testImplementation("io.mockk:mockk:1.13.12")
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
    testImplementation("app.cash.turbine:turbine:1.2.0") // For Flow testing
}

File Location

Mirror the source path:

  • Source: app/src/main/java/com/example/feature/MyViewModel.kt
  • Test: app/src/test/java/com/example/feature/MyViewModelTest.kt

Test Structure

class MyViewModelTest {

    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    private val repository: MyRepository = mockk()
    private lateinit var viewModel: MyViewModel

    @Before
    fun setUp() {
        viewModel = MyViewModel(repository)
    }

    @Test
    fun `loadData sets state to Success when repository returns data`() = runTest {
        // Given
        val expected = listOf(Item("1", "Test"))
        coEvery { repository.getData() } returns expected

        // When
        viewModel.loadData()

        // Then
        assertEquals(UiState.Success(expected), viewModel.state.value)
        coVerify(exactly = 1) { repository.getData() }
    }

    @Test
    fun `loadData sets state to Error when repository throws`() = runTest {
        // Given
        coEvery { repository.getData() } throws IOException("Network error")

        // When
        viewModel.loadData()

        // Then
        assertTrue(viewModel.state.value is UiState.Error)
    }
}

MainDispatcherRule (for ViewModel tests)

class MainDispatcherRule(
    private val dispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {
    override fun starting(description: Description) {
        Dispatchers.setMain(dispatcher)
    }
    override fun finished(description: Description) {
        Dispatchers.resetMain()
    }
}

MockK Cheat Sheet

// Create mock
val repo: Repository = mockk()
val repo: Repository = mockk(relaxed = true) // Returns defaults for unconfigured calls

// Stub behavior
every { repo.getData() } returns listOf(item)
coEvery { repo.fetchData() } returns result       // For suspend functions
every { repo.getData() } throws IOException()
every { repo.save(any()) } just runs               // Void functions

// Verify
verify { repo.getData() }
verify(exactly = 1) { repo.save(any()) }
coVerify { repo.fetchData() }
verify(exactly = 0) { repo.delete(any()) }         // Never called

// Capture arguments
val slot = slot<String>()
every { repo.search(capture(slot)) } returns emptyList()
// After call: slot.captured == "search term"

// Mock objects/companions
mockkObject(MySingleton)
every { MySingleton.instance } returns mockk()

Flow Testing with Turbine

@Test
fun `events flow emits navigation event`() = runTest {
    viewModel.events.test {
        viewModel.onSubmitClicked()
        assertEquals(Event.NavigateToSuccess, awaitItem())
        cancelAndConsumeRemainingEvents()
    }
}

Compose UI Testing

Setup

dependencies {
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-test-manifest")
}

Test Structure

class MyScreenTest {
    @get:Rule
    val composeRule = createComposeRule()

    @Test
    fun submitButton_isDisabled_whenFieldIsEmpty() {
        composeRule.setContent {
            MyScreen(state = MyState(text = ""))
        }

        composeRule.onNodeWithText("Submit").assertIsNotEnabled()
    }

    @Test
    fun errorMessage_isDisplayed_whenStateIsError() {
        composeRule.setContent {
            MyScreen(state = MyState(error = "Something went wrong"))
        }

        composeRule.onNodeWithText("Something went wrong").assertIsDisplayed()
    }
}

Espresso (XML Views)

@Test
fun clickSubmit_showsConfirmation() {
    onView(withId(R.id.btn_submit)).perform(click())
    onView(withText("Confirmed")).check(matches(isDisplayed()))
}

Running Tests

# All unit tests
./gradlew testDebugUnitTest

# Specific flavor
./gradlew test<Flavor>DebugUnitTest

# Single test class
./gradlew testDebugUnitTest --tests "com.example.MyViewModelTest"

# Single test method
./gradlew testDebugUnitTest --tests "com.example.MyViewModelTest.loadData sets state to Success*"

# Instrumented tests (requires device/emulator)
./gradlew connectedDebugAndroidTest

Common Patterns

Testing Use Cases

@Test
fun `invoke returns transformed data`() = runTest {
    coEvery { repository.getItems() } returns rawItems
    val result = useCase()
    assertEquals(expectedTransformed, result)
}

Testing Repository with Room

// Use in-memory database for tests
val db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
val dao = db.myDao()

Testing Mapper/Converter Functions

@Test
fun `toEntity maps all fields correctly`() {
    val dto = MyDto(id = "1", name = "Test", value = 42.0)
    val entity = dto.toEntity()
    assertEquals("1", entity.id)
    assertEquals("Test", entity.name)
    assertEquals(42.0, entity.value, 0.001)
}

Troubleshooting

Tests pass locally but fail in CI

  • Check timezone-dependent tests
  • Check tests that depend on system locale
  • Check flaky tests with @FlakyTest or increase timeouts

MockK issues with final classes

Add to src/test/resources/mockk-extensions/io.mockk.ifc:

io.mockk.classmocking.enabled=true

Coroutine test timing issues

Use advanceUntilIdle() or runCurrent() in runTest blocks.

Keep looking

Skills are one crate of 626,474. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.