JUnit and Test Structure
JUnit and Test Structure
JUnit 4 is the standard test framework for Android. Every test class follows the same pattern:
@RunWith(MockKJUnitRunner::class)
class LoginViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@MockK
private lateinit var authRepository: AuthRepository
private lateinit var viewModel: LoginViewModel
@Before
fun setup() {
MockKAnnotations.init(this)
viewModel = LoginViewModel(authRepository)
}
@Test
fun `login with valid credentials navigates to home`() = runTest {
coEvery { authRepository.login(any(), any()) } returns Result.success(User("test"))
viewModel.login("test@example.com", "password")
val state = viewModel.uiState.first()
assertThat(state).isInstanceOf(LoginUiState.Success::class.java)
}
@Test
fun `login with empty email shows error`() = runTest {
viewModel.login("", "password")
val state = viewModel.uiState.first()
assertThat(state).isInstanceOf(LoginUiState.Error::class.java)
assertThat((state as LoginUiState.Error).message).contains("email")
}
}
Key Annotations
@Test— Marks a method as a test.@Before— Runs before each test. Use for setup.@After— Runs after each test. Use for cleanup.@BeforeClass/@AfterClass— Runs once per test class. Use for expensive setup.@Rule— Adds reusable test behavior (rules, watchers, dispatchers).@MockK— Creates a MockK mock for a dependency.
Given-When-Then
Every test follows the Arrange-Act-Assert pattern:
- Given — Set up preconditions and mock behavior.
- When — Call the method under test.
- Assert — Verify the result matches expectations.
This structure makes tests readable and maintainable.
MockK Deep Dive
MockK Deep Dive
MockK is the Kotlin-native mocking library. It handles coroutines, final classes, and extension functions naturally.
Creating Mocks
// Regular mock
private val repository = mockk<UserRepository>()
// Relaxed mock (returns default values for unstubbed calls)
private val repository = mockk<UserRepository>(relaxed = true)
// Spy (real object with selective mocking)
private val repository = spyk(RealUserRepository())
Stubbing
// Suspend function
coEvery { repository.getUser("123") } returns User(name = "Alice")
// Regular function
every { repository.getCachedUser() } returns null
// Throwing an exception
every { repository.getUser("bad") } throws IllegalArgumentException("Invalid ID")
// Sequential returns
coEvery { repository.getUser(any()) } returnsMany listOf(
User(name = "Alice"),
User(name = "Bob")
)
Verifying Interactions
// Verify called once
coVerify(exactly = 1) { repository.getUser("123") }
// Verify never called
coVerify(exactly = 0) { repository.deleteUser(any()) }
// Verify ordering
coVerifyOrder {
repository.getUser("123")
repository.cacheUser(any())
}
// Verify no other interactions
coVerifyAll {
repository.getUser("123")
repository.cacheUser(any())
}
Mocking Flows
@Test
fun `notifications flow emits unread count`() = runTest {
val notifications = flowOf(listOf(
Notification(id = "1", read = false),
Notification(id = "2", read = true)
))
every { notificationRepository.getNotifications() } returns notifications
viewModel.unreadCount.test {
assertThat(awaitItem()).isEqualTo(1)
cancelAndIgnoreRemainingEvents()
}
}
Testing ViewModels
Testing ViewModels
ViewModels hold UI state and business logic. They are the most testable layer because they have no Android framework dependencies when written correctly.
ViewModel Test Pattern
@RunWith(MockKJUnitRunner::class)
class ProductListViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@MockK
private lateinit var productRepository: ProductRepository
private lateinit var viewModel: ProductListViewModel
@Before
fun setup() {
MockKAnnotations.init(this)
viewModel = ProductListViewModel(productRepository)
}
@Test
fun `initial state is Loading`() = runTest {
val state = viewModel.uiState.first()
assertThat(state).isInstanceOf(ProductListUiState.Loading::class.java)
}
@Test
fun `load products success shows products`() = runTest {
coEvery { productRepository.getProducts() } returns listOf(
Product(id = "1", name = "Widget", price = 9.99)
)
viewModel.loadProducts()
val state = viewModel.uiState.first()
assertThat(state).isInstanceOf(ProductListUiState.Success::class.java)
assertThat((state as ProductListUiState.Success).products).hasSize(1)
}
@Test
fun `load products error shows error`() = runTest {
coEvery { productRepository.getProducts() } throws IOException("Network error")
viewModel.loadProducts()
val state = viewModel.uiState.first()
assertThat(state).isInstanceOf(ProductListUiState.Error::class.java)
}
}
Testing SavedStateHandle
ViewModels often receive parameters through SavedStateHandle:
@Test
fun `product id is extracted from saved state handle`() {
val savedStateHandle = SavedStateHandle(mapOf("productId" to "42"))
val viewModel = ProductDetailViewModel(savedStateHandle, productRepository)
assertThat(viewModel.productId).isEqualTo("42")
}
Common ViewModel Test Scenarios
- Initial state — Verify the ViewModel starts in the correct state.
- Happy path — Verify success states with correct data.
- Error handling — Verify error states with meaningful messages.
- Loading states — Verify loading appears and disappears.
- Side effects — Verify navigation events, snackbar messages.
- Edge cases — Empty lists, null values, network timeouts.
Quiz
1. What is the difference between every and coEvery in MockK?
2. What does a relaxed mock return for unstubbed calls?
3. Why use spyk instead of mockk?
4. What does coVerifyAll do that coVerify does not?
Flashcards
Question
What is the difference between mockk and spyk?
Click to reveal answer
Answer
mockk creates a fully fake object with no real behavior. spyk wraps a real object and lets you selectively override specific methods.
Question
How do you stub a suspend function in MockK?
Click to reveal answer
Answer
Use coEvery { } instead of every { }. Example: coEvery { repo.getUser(any()) } returns User("Alice")
Question
What does MainDispatcherRule do?
Click to reveal answer
Answer
It replaces Dispatchers.Main with a test dispatcher so ViewModel tests do not crash when code uses withContext(Dispatchers.Main).
Question
What are the three phases of Given-When-Then?
Click to reveal answer
Answer
Given: set up preconditions and mocks. When: call the method under test. Assert: verify the result matches expectations.
Revision Notes
Key Takeaways
- 1. Use coEvery and coVerify for suspend functions, not every and verify
- 2. Relaxed mocks return defaults but can hide missing stubs — prefer explicit stubbing
- 3. ViewModels are the most testable layer when they have no Android framework deps
- 4. Always test initial state, success, error, and edge cases
- 5. Use MainDispatcherRule to handle Dispatchers.Main in tests
Interview Tips
- • Explain the difference between mocks and spies and when to use each
- • Walk through how you would test a ViewModel that fetches data from a repository
- • Discuss how you handle coroutine testing in unit tests
- • Know the Given-When-Then pattern and why it matters for test readability
Cheat Sheet
Unit Testing Cheat Sheet
JUnit 4 Annotations:
- @Test — test method
- @Before — runs before each test
- @MockK — creates a mock
- @get:Rule — adds test rules
MockK Core:
- mockk
() — create mock - spyk(T()) — wrap real object
- every { } / coEvery { } — stub
- verify { } / coVerify { } — check interactions
- coVerifyAll { } — no other interactions
ViewModel Testing Pattern:
- Mock dependencies with @MockK
- Set up Dispatchers with MainDispatcherRule
- Test initial state, happy path, error path
- Use Turbine or first() to collect state
Coroutine Testing:
- runTest { } — test coroutine scope
- coEvery / coVerify — for suspend functions
- TestDispatcher — control timing