Android Testing in 2025: Unit, UI, and Integration Tests

A reliable test suite is the best tool for moving fast without breaking things. In 2025 the Android testing ecosystem has matured significantly — this guide covers the stack that works and the patterns that make tests actually useful rather than a maintenance burden.

The Testing Pyramid

For Android apps, a healthy distribution is roughly:

  • 70% Unit tests — fast, isolated, no emulator. Business logic, ViewModels, mappers, use-cases.
  • 20% Integration tests — test a slice of the system (e.g., Room DAO, repository with a fake API).
  • 10% UI / End-to-end tests — full flows on a real device or emulator.

Unit Testing with JUnit 5 and MockK

MockK is the Kotlin-first mocking library. It handles coroutines, object mocking, and sealed classes naturally.

@ExtendWith(MockKExtension::class)
class LoginViewModelTest {
    @MockK lateinit var authRepo: AuthRepository
    private lateinit var viewModel: LoginViewModel

    @BeforeEach
    fun setUp() { viewModel = LoginViewModel(authRepo) }

    @Test
    fun `login success emits LoggedIn state`() = runTest {
        coEvery { authRepo.login(any(), any()) } returns User(id = "1")
        viewModel.login("[email protected]", "password")
        assertEquals(LoginState.LoggedIn, viewModel.state.value)
    }
}

Testing Room DAOs

Use an in-memory Room database — it is fast and does not require a device.

@RunWith(AndroidJUnit4::class)
class ArticleDaoTest {
    private lateinit var db: AppDatabase
    private lateinit var dao: ArticleDao

    @Before
    fun setup() {
        db = Room.inMemoryDatabaseBuilder(
            ApplicationProvider.getApplicationContext(), AppDatabase::class.java
        ).allowMainThreadQueries().build()
        dao = db.articleDao()
    }

    @After
    fun tearDown() { db.close() }

    @Test
    fun insertAndRetrieve() = runTest {
        val article = Article(id = "1", title = "Test")
        dao.insert(article)
        assertEquals(article, dao.getById("1").first())
    }
}

Testing Compose UI

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun loginButton_isDisabledWhenFieldsEmpty() {
    composeTestRule.setContent { LoginScreen(onLogin = {}) }
    composeTestRule.onNodeWithTag("login_button").assertIsNotEnabled()
}

@Test
fun loginButton_isEnabledWhenFieldsFilled() {
    composeTestRule.setContent { LoginScreen(onLogin = {}) }
    composeTestRule.onNodeWithTag("email_field").performTextInput("[email protected]")
    composeTestRule.onNodeWithTag("password_field").performTextInput("password")
    composeTestRule.onNodeWithTag("login_button").assertIsEnabled()
}

Testing Flows with Turbine

@Test
fun `search query updates results`() = runTest {
    viewModel.results.test {
        assertEquals(emptyList(), awaitItem())
        viewModel.onSearch("kotlin")
        assertEquals(fakeResults, awaitItem())
        cancelAndIgnoreRemainingEvents()
    }
}

CI Integration

  • Run unit tests on every pull request — they are fast enough (<1 min for most projects).
  • Run instrumented tests on a schedule or pre-merge for critical paths.
  • Use Firebase Test Lab or GitHub Actions with an emulator for device testing in CI.