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.