HuxleyMc

android-test-generator

1
0
# Install this skill:
npx skills add HuxleyMc/Android-Skills --skill "android-test-generator"

Install specific skill from multi-skill repository

# Description

Generates comprehensive test suites for Android projects. Use when creating unit tests for ViewModels/Repositories, UI tests for Compose, integration tests, or property-based tests. Covers Kotest, JUnit, MockK, and coroutine testing.

# SKILL.md


name: android-test-generator
description: Generates comprehensive test suites for Android projects. Use when creating unit tests for ViewModels/Repositories, UI tests for Compose, integration tests, or property-based tests. Covers Kotest, JUnit, MockK, and coroutine testing.
tags: ["android", "testing", "kotest", "junit", "mockk", "compose-testing", "unit-test", "coroutines"]
difficulty: intermediate
category: testing
version: "1.0.0"
last_updated: "2025-01-29"


Android Test Generator

Quick Start

Generate tests based on component type:

1. ViewModel Tests (StateFlow, coroutines)
2. Repository Tests (data sources, caching)
3. UseCase Tests (business logic)
4. Compose UI Tests
5. Integration Tests
6. Property-Based Tests

Test Templates

ViewModel Tests

Complete Test Class:

@ExperimentalCoroutinesApi
class UserViewModelTest : FunSpec({

    coroutineTestScope = true

    lateinit var repository: UserRepository
    lateinit var viewModel: UserViewModel

    beforeEach {
        repository = mockk()
        viewModel = UserViewModel(repository)
    }

    context("loadUser") {

        test("emits loading then success state") {
            val user = User("1", "John", "[email protected]")
            coEvery { repository.getUser("1") } coAnswers {
                delay(100)
                user
            }

            viewModel.loadUser("1")

            viewModel.uiState.value.isLoading shouldBe true
            advanceTimeBy(100)

            assertSoftly(viewModel.uiState.value) {
                isLoading shouldBe false
                this.user?.name shouldBe "John"
                error shouldBe null
            }
        }

        test("emits error state on failure") {
            coEvery { repository.getUser(any()) } throws 
                IOException("Network error")

            viewModel.loadUser("1")
            advanceUntilIdle()

            assertSoftly(viewModel.uiState.value) {
                isLoading shouldBe false
                user shouldBe null
                error shouldBe "Network error"
            }
        }

        test("cancels previous load on new request") {
            coEvery { repository.getUser("1") } coAnswers {
                delay(1000)
                User("1", "Old")
            }
            coEvery { repository.getUser("2") } returns 
                User("2", "New")

            viewModel.loadUser("1")
            viewModel.loadUser("2")
            advanceUntilIdle()

            viewModel.uiState.value.user?.name shouldBe "New"
        }
    }

    context("search") {

        test("debounces search query") {
            val users = listOf(User("1", "John"))
            coEvery { repository.searchUsers(any()) } returns users

            viewModel.setQuery("j")
            viewModel.setQuery("jo")
            viewModel.setQuery("john")

            coVerify(exactly = 0) { repository.searchUsers(any()) }
            advanceTimeBy(300)
            coVerify(exactly = 1) { repository.searchUsers("john") }
        }
    }
})

Repository Tests

With MockK:

class UserRepositoryTest : BehaviorSpec({

    val api = mockk<UserApi>()
    val dao = mockk<UserDao>()
    val repository = UserRepository(api, dao)

    given("getUser") {
        val userId = "123"
        val cachedUser = User(userId, "Cached User")
        val networkUser = User(userId, "Network User")

        `when`("user exists in cache") {
            coEvery { dao.getUser(userId) } returns cachedUser

            then("returns cached user without network call") {
                val result = repository.getUser(userId)

                result shouldBe cachedUser
                coVerify(exactly = 0) { api.fetchUser(any()) }
            }
        }

        `when`("user not in cache") {
            coEvery { dao.getUser(userId) } returns null
            coEvery { api.fetchUser(userId) } returns networkUser
            coEvery { dao.insert(networkUser) } just Runs

            then("fetches from network and caches") {
                val result = repository.getUser(userId)

                result shouldBe networkUser
                coVerify { api.fetchUser(userId) }
                coVerify { dao.insert(networkUser) }
            }
        }

        `when`("network fails") {
            coEvery { dao.getUser(userId) } returns null
            coEvery { api.fetchUser(any()) } throws IOException()

            then("throws exception") {
                shouldThrow<IOException> {
                    repository.getUser(userId)
                }
            }
        }
    }
})

Compose UI Tests

@HiltAndroidTest
class UserListScreenTest {

    @get:Rule
    val hiltRule = HiltAndroidRule(this)

    @get:Rule
    val composeTestRule = createAndroidComposeRule<MainActivity>()

    @Test
    fun userList_displaysUsers() {
        composeTestRule.setContent {
            AppTheme {
                UserListScreen()
            }
        }

        composeTestRule.onNodeWithText("John Doe").assertExists()
        composeTestRule.onNodeWithText("Jane Smith").assertExists()
    }

    @Test
    fun userList_clickNavigatesToDetail() {
        composeTestRule.setContent {
            AppTheme {
                UserListScreen(onUserClick = { id ->
                    assertEquals("1", id)
                })
            }
        }

        composeTestRule.onNodeWithText("John Doe")
            .performClick()
    }

    @Test
    fun userList_scrollToBottom() {
        composeTestRule.setContent {
            AppTheme {
                UserListScreen()
            }
        }

        composeTestRule.onNodeWithTag("user_list")
            .performScrollToNode(hasText("User 50"))
    }
})

Property-Based Tests

class CalculatorPropertyTest : StringSpec({

    "addition is commutative" {
        checkAll<Int, Int> { a, b ->
            a + b shouldBe b + a
        }
    }

    "reversing a string twice returns original" {
        checkAll<String> { str ->
            str.reversed().reversed() shouldBe str
        }
    }

    "list size after adding element increases by 1" {
        checkAll(Arb.list(Arb.int()), Arb.int()) { list, element ->
            (list + element).size shouldBe list.size + 1
        }
    }
})

Data-Driven Tests

class CalculatorTest : FunSpec({

    context("addition") {
        withData(
            Pair(1, 1) to 2,
            Pair(2, 3) to 5,
            Pair(0, 0) to 0,
            Pair(-1, 1) to 0
        ) { (input, expected) ->
            val (a, b) = input
            a + b shouldBe expected
        }
    }

    context("division") {
        withData(
            nameFn = { "${it.first} / ${it.second} = ${it.third}" },
            Triple(10, 2, 5),
            Triple(9, 3, 3),
            Triple(100, 10, 10)
        ) { (a, b, expected) ->
            a / b shouldBe expected
        }
    }
})

Test Data Builders

class UserBuilder {
    var id: String = "1"
    var name: String = "John Doe"
    var email: String = "[email protected]"
    var age: Int = 30

    fun build() = User(id, name, email, age)
}

fun user(block: UserBuilder.() -> Unit = {}) = 
    UserBuilder().apply(block).build()

// Usage in tests
val user = user {
    name = "Jane"
    email = "[email protected]"
}

Examples (Input β†’ Output)

Generate ViewModel Tests

Input: "Create tests for this ViewModel"

class ProductViewModel(
    private val repository: ProductRepository
) : ViewModel() {

    private val _products = MutableStateFlow<List<Product>>(emptyList())
    val products = _products.asStateFlow()

    fun loadProducts() {
        viewModelScope.launch {
            _products.value = repository.getProducts()
        }
    }
}

Output:

class ProductViewModelTest : FunSpec({

    coroutineTestScope = true

    lateinit var repository: ProductRepository
    lateinit var viewModel: ProductViewModel

    beforeEach {
        repository = mockk()
        viewModel = ProductViewModel(repository)
    }

    test("loadProducts updates state") {
        val products = listOf(
            Product("1", "Product 1", 10.0),
            Product("2", "Product 2", 20.0)
        )
        coEvery { repository.getProducts() } returns products

        viewModel.loadProducts()
        advanceUntilIdle()

        viewModel.products.value shouldBe products
    }

    test("loadProducts handles error") {
        coEvery { repository.getProducts() } throws 
            IOException("Network error")

        viewModel.loadProducts()
        advanceUntilIdle()

        viewModel.products.value shouldBe emptyList()
    }
})

Resources

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.