Working with Testing
1. Writing Unit Tests
Example: Basic Unit Test
import org.junit.Test
import org.junit.Assert.*
class CalculatorTest {
@Test
fun testAddition() {
val calculator = Calculator()
val result = calculator.add(2, 3)
assertEquals(5, result)
}
}
| Feature | Description | Notes |
|---|---|---|
| JUnit 5 | org.junit.jupiter.api.Test |
Framework for writing unit tests |
| Test Annotation | @Test |
Mark test methods |
| Assertion Methods | assertEquals, assertTrue, assertFalse |
Validate test outcomes |
Note: Unit tests run on the JVM and do not require Android instrumentation.
Example: Mocking with Mockito
import org.mockito.Mockito.*
class UserServiceTest {
@Test
fun testFetchUser() {
val mockRepository = mock(UserRepository::class.java)
val userService = UserService(mockRepository)
`when`(mockRepository.fetchUser(1)).thenReturn(User("John"))
val user = userService.fetchUser(1)
assertEquals("John", user.name)
}
}
2. Using Assertions
| Assertion Type | Description | Usage Example | Notes |
|---|---|---|---|
| assertTrue | Checks that a condition is true | assertTrue(result == 5) |
Useful for verifying boolean conditions |
| assertFalse | Checks that a condition is false | assertFalse(isError) |
Opposite of assertTrue |
| assertEquals | Checks that two values are equal | assertEquals(42, calculateValue()) |
Compares values using == |
| assertNull | Checks that a value is null | assertNull(user?.name) |
Verifies that object is not initialized |
| assertNotNull | Checks that a value is not null | assertNotNull(user?.name) |
Ensures object has been initialized |
| assertThrows | Checks that a specific exception is thrown | |
Useful for testing error handling |
Example: Using assertThrows
fun processInput(input: String): Int {
if (input.isEmpty()) throw IllegalArgumentException("Input cannot be empty")
return input.length
}
@Test
fun testProcessInputWithEmptyString() {
assertThrows<IllegalArgumentException> {
processInput("")
}
}
Note: Assertions in Kotlin tests are typically part of the JUnit framework and can be extended with custom logic using
assertThat from Hamcrest or Truth libraries.3. Mocking Dependencies
| Feature | Description | Notes |
|---|---|---|
| Mockito-Kotlin | Library for mocking in Kotlin tests | Extension functions for Mockito, simplifies test setup |
| Mockito | Java-based mocking framework | Works with Kotlin but requires more boilerplate |
| MockK | Native Kotlin mocking library | More idiomatic for Kotlin, supports coroutines |
Example: Using MockK to mock a repository
import io.mockk.*
class UserRepositoryTest {
private val mockRepository = mockk<UserRepository>()
private val userService = UserService(mockRepository)
@Test
fun `get user returns correct data`() {
every { mockRepository.getUser(1) } returns User(1, "John")
val result = userService.fetchUser(1)
assertEquals("John", result.name)
verify { mockRepository.getUser(1) }
}
}
Example: Mocking suspending functions
import io.mockk.*
class ApiServiceTest {
private val mockApi = mockk<ApiService>()
@Test
fun `fetch data with delay`() = runTest {
coEvery { mockApi.fetchData() } returns "mocked data"
val result = mockApi.fetchData()
assertEquals("mocked data", result)
}
}
Note: MockK is preferred over Mockito for Kotlin due to better integration with Kotlin's features like coroutines and extension functions.
4. Testing ViewModels
Example: Basic ViewModel Test
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class UserViewModel : ViewModel() {
private val _user = MutableStateFlow("")
val user: StateFlow<String> = _user
fun fetchUser(userId: String) {
viewModelScope.launch {
// Simulate API call
_user.value = "User: $userId"
}
}
}
// Test class
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.mockito.junit.MockitoJUnitRunner
@RunWith(MockitoJUnitRunner::class)
class UserViewModelTest {
@get:Rule
val rule = InstantTaskExecutorRule()
private lateinit var viewModel: UserViewModel
@Before
fun setup() {
viewModel = UserViewModel()
}
@Test
fun `fetchUser updates user state`() {
viewModel.fetchUser("123")
assert(viewModel.user.value == "User: 123")
}
}
| Feature | Description | Notes |
|---|---|---|
| ViewModel | androidx.lifecycle.ViewModel |
Base class for ViewModels in Android |
| StateFlow | kotlinx.coroutines.flow.StateFlow |
Immutable flow that holds the latest value and emits it to new collectors |
| MutableStateFlow | kotlinx.coroutines.flow.MutableStateFlow |
Mutable version of StateFlow for updating values |
| viewModelScope | androidx.lifecycle.viewModelScope |
Coroutine scope tied to the ViewModel's lifecycle |
Note: Use
InstantTaskExecutorRule to ensure synchronous execution of tasks in tests.Testing ViewModels - Key Points
- ViewModels should be tested for state updates and coroutine execution
- Use
StateFlowto observe state changes in tests - Test
viewModelScopefor proper coroutine handling - Mock external dependencies like API calls in tests
5. Writing Instrumentation Tests
Instrumentation tests in Android-Kotlin are used to test the interaction between your app and the Android framework. These tests run on an Android device or emulator.
| Test Type | Description | Use Case |
|---|---|---|
| Espresso | UI testing framework for Android | Testing UI interactions and assertions |
| UI Automator | Testing across multiple apps | Testing system-level UI and cross-app interactions |
Example: Espresso Test
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.ActivityTestRule
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@get:Rule
val activityRule = ActivityTestRule(MainActivity::class.java)
@Test
fun testButtonPress() {
onView(withId(R.id.button)).perform(click())
}
}
Note: Ensure that your test dependencies are included in
build.gradle.Warning: Instrumentation tests run on the device/emulator and are slower than unit tests.
Key Points
- Use
Espressofor UI component testing. - Use
UI Automatorfor cross-app or system-level tests. - Tests must be placed in
src/androidTest/java.
6. Using Espresso for UI Testing
Espresso is a testing framework for Android that allows you to write concise and reliable UI tests. It provides a rich set of APIs to interact with UI components and verify their behavior.
| Component | Description | Usage |
|---|---|---|
| Espresso | Framework for UI testing in Android | Used to interact with UI elements and verify their state |
| ViewMatchers | Used to find views in the UI | onView(withId(R.id.button)).perform(click()) |
| ViewActions | Used to perform actions on views | perform(click()) |
| ViewAssertions | Used to assert the state of views | check(matches(isDisplayed())) |
Example: Basic Espresso Test
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun testButtonIsDisplayed() {
onView(withId(R.id.button)).check(matches(isDisplayed()))
}
@Test
fun testButtonClick() {
onView(withId(R.id.button)).perform(click())
}
}
Example: Testing EditText Input
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.text
@Test
fun testEditTextInput() {
onView(withId(R.id.editText)).perform(typeText("Hello World"))
onView(withId(R.id.editText)).check(matches(text("Hello World")))
}
Note: Espresso tests run on the Android device or emulator and simulate user interactions with the UI.
Warning: Make sure to use
ActivityScenarioRule or ActivityTestRule to launch the activity under test.
Key Features of Espresso
- Easy to use and understand
- Provides a rich set of matchers for finding views
- Supports complex UI interactions
- Can be integrated with JUnit for test automation
7. Testing RecyclerView Items
Example: Basic RecyclerView Item Test
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.action.ViewActions.click
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class RecyclerViewItemTest {
@Test
fun testItemClick() {
onView(withId(R.id.recyclerView))
.perform(RecyclerViewActions.actionOnItemAtPosition<MyAdapter.ViewHolder>(0, click()))
}
}
| Method | Description | Notes |
|---|---|---|
| onView(withId(...)) | Locates a view in the current layout | Used to target RecyclerView |
| RecyclerViewActions.actionOnItemAtPosition() | Performs an action on a specific item in the RecyclerView | Requires adapter ViewHolder type parameter |
Note: Ensure the RecyclerView item is visible and interactable before performing actions.
Warning: Using
RecyclerViewActions requires the espresso-contrib dependency.Example: Testing Item Content
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.espresso.assertion.ViewAssertions.matches
@Test
fun testItemContent() {
onView(withId(R.id.recyclerView))
.perform(RecyclerViewActions.actionOnItemAtPosition<MyAdapter.ViewHolder>(0, click()))
onView(withText("Item Title"))
.check(matches(isDisplayed()))
}
8. Implementing Idling Resources
Idling Resources in Android are used to signal when asynchronous operations (like network calls or database queries) are complete, allowing the UI testing framework (such as Espresso) to wait for these operations to finish before proceeding with tests.
| Property | Description | Usage |
|---|---|---|
| IdlingResource | Interface that defines a resource that can be monitored for idleness. | Implement this interface to create custom idling resources. |
| registerIdleCallback | Method to register a callback that is invoked when the resource becomes idle. | Used by testing frameworks to know when it's safe to proceed with the test. |
| unregisterIdleCallback | Method to unregister the idle callback. | Used when the resource is no longer needed for testing. |
Example: Custom Idling Resource for Network Calls
class NetworkIdlingResource(
private val name: String,
private val requestCount: AtomicInteger = AtomicInteger(0)
) : IdlingResource {
private var callback: IdlingResource.ResourceCallback? = null
override fun getName(): String = name
override fun isIdleNow(): Boolean = requestCount.get() == 0
override fun registerIdleCallback(callback: IdlingResource.ResourceCallback?) {
this.callback = callback
}
override fun unregisterIdleCallback(callback: IdlingResource.ResourceCallback?) {
this.callback = null
}
fun increment() {
requestCount.incrementAndGet()
}
fun decrement() {
val count = requestCount.decrementAndGet()
if (count == 0) {
callback?.onTransitionToIdle()
}
}
}
Example: Using Idling Resource in Espresso Test
class NetworkTest {
private lateinit var idlingResource: NetworkIdlingResource
@Before
fun setup() {
idlingResource = NetworkIdlingResource("Network")
// Register the idling resource with Espresso
IdlingRegistry.getInstance().register(idlingResource)
}
@Test
fun testNetworkCall() {
idlingResource.increment()
// Perform network request
// Espresso will wait until decrement() is called
idlingResource.decrement()
}
@After
fun tearDown() {
IdlingRegistry.getInstance().unregister(idlingResource)
}
}
Note: Make sure to register and unregister the IdlingResource in your test setup and teardown to avoid interference with other tests.
9. Testing Intents
| Feature | Description | Notes |
|---|---|---|
| Intent | Intent is used to communicate between components in Android. |
Used for starting Activities, Services, and broadcasting |
| Testing Intent | Testing intent involves verifying that the correct intent is launched with proper data. | Use Intent in unit tests to simulate component interactions |
Example: Testing Intent with Mockito
import org.mockito.Mockito
import android.content.Intent
import android.content.Context
class IntentTest {
fun testIntentLaunch() {
val context = Mockito.mock(Context::class.java)
val intent = Intent(context, TargetActivity::class.java)
intent.putExtra("key", "value")
Mockito.verify(context).startActivity(intent)
}
}
Note: Ensure that the
Intent is correctly configured with all necessary extras and flags before testing.
10. Testing Compose UI
Example: Basic Compose UI Test
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class MyComposeTest {
@get:Rule
val composeRule = createAndroidComposeRule<MainActivity>()
@Test
fun testButtonDisplaysCorrectText() {
composeRule.onNodeWithText("Click me")
.assertIsDisplayed()
.performClick()
}
}
| Method | Description | Notes |
|---|---|---|
| onNodeWithText() | Find a node by its text content | Useful for UI element identification |
| assertIsDisplayed() | Check if a node is visible | Ensures the UI element is rendered |
| performClick() | Simulate a click on the node | Triggers the associated action |
Note: Compose tests require
androidx.compose.ui.test dependencies.Warning: Ensure
createAndroidComposeRule is used for Android-specific tests.Testing Compose UI - Key Points
- Use
createAndroidComposeRuleto set up the test environment - Find UI elements using
onNodeWithText()or other selectors - Assert visibility and interactions using
assertIsDisplayed()andperformClick()
11. Running Tests
Example: Running Unit Tests
class ExampleTest {
@Test
fun testAddition() {
val result = 2 + 2
assertEquals(4, result)
}
}
| Command | Description | Notes |
|---|---|---|
| ./gradlew test | Run all unit tests | Executes tests in the test source set |
| ./gradlew connectedAndroidTest | Run all instrumentation tests | Runs tests on a physical or emulated device |
| ./gradlew testDebugUnitTest | Run debug unit tests | Runs tests in the debug build variant |
Note: Ensure that your Android project is properly configured with the necessary test dependencies in
build.gradle.