// build.gradle.kts
dependencies {
testImplementation("junit:junit:4.13.2")
testImplementation("org.mockito:mockito-core:5.8.0")
testImplementation("io.mockk:mockk:1.13.8")
}
// ViewModel Unit Test
class UserViewModelTest {
private lateinit var viewModel: UserViewModel
private val mockRepo = mock<UserRepository>()
@Before
fun setup() {
viewModel = UserViewModel(mockRepo)
}
@Test
fun `loadUsers should update users LiveData`() = runTest {
val mockUsers = listOf(User(1, "Alice", "alice@example.com"))
`when`(mockRepo.getUsers()).thenReturn(mockUsers)
viewModel.loadUsers()
val result = viewModel.users.getOrAwaitValue()
assertEquals(1, result.size)
assertEquals("Alice", result[0].name)
}
@Test
fun `loadUsers should handle empty response`() = runTest {
`when`(mockRepo.getUsers()).thenReturn(emptyList())
viewModel.loadUsers()
val result = viewModel.users.getOrAwaitValue()
assertTrue(result.isEmpty())
}
}
UI Tests with Espresso
// build.gradle.kts
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.espresso:espresso-contrib:3.5.1")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun buttonClick_updatesTextView() {
// Find the button and click it
onView(withId(R.id.clickMeButton))
.perform(click())
// Check the text view shows expected text
onView(withId(R.id.greetingText))
.check(matches(withText("Hello, Android!")))
}
@Test
fun recyclerView_scrolls() {
onView(withId(R.id.recyclerView))
.perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(
5, click()
))
onView(withText("Item 6"))
.check(matches(isDisplayed()))
}
}
Mockito & MockK
// Mockito (Java-style)
@RunWith(MockitoJUnitRunner::class)
class LoginViewModelTest {
@Mock lateinit var authRepo: AuthRepository
lateinit var viewModel: LoginViewModel
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
viewModel = LoginViewModel(authRepo)
}
@Test
fun login_success() {
`when`(authRepo.login("user", "pass"))
.thenReturn(Result.success(User("Alice")))
viewModel.login("user", "pass")
assertTrue(viewModel.loginResult.value?.isSuccess == true)
}
}
// MockK (Kotlin-native)
class MockKTest {
val mockRepo = mockk<AuthRepository>()
@Test
fun loginFailsWithWrongPassword() {
every { mockRepo.login("user", "wrong") }
.returns(Result.failure(Exception("Invalid credentials")))
val result = mockRepo.login("user", "wrong")
assertTrue(result.isFailure)
}
}
Debugging Tools
// Logging
Log.d("MainActivity", "Button clicked at ${System.currentTimeMillis()}")
Log.e("Network", "Request failed: ${error.message}")
Log.i("Lifecycle", "Activity created")
// Android Profiler (in Android Studio)
// View > Tool Windows > Profiler
// Monitor: CPU, Memory, Network, Energy
// StrictMode (catch accidental disk/network on main thread)
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build())
}
// LeakCanary (memory leak detection)
// implementation("com.squareup.leakcanary:leakcanary-android:2.12")
Testing Checklist
Test Type
Framework
What to Test
Unit
JUnit + MockK
ViewModels, Repositories, UseCases
Integration
JUnit + Room
Database, Content Providers
UI
Espresso
Activities, Fragments, Navigation
End-to-End
Compose UI Test
Full user flows
Exercise: Write unit tests for a LoginViewModel using MockK. Mock the AuthRepository and test three scenarios: login succeeds, login fails with wrong password, and login fails with network error. Then write an Espresso UI test that enters credentials, clicks login, and verifies the welcome message appears.