Skip to main content

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
assertThrows<IllegalArgumentException> {
    processInput("")
}
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 StateFlow to observe state changes in tests
  • Test viewModelScope for 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 Espresso for UI component testing.
  • Use UI Automator for 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 createAndroidComposeRule to set up the test environment
  • Find UI elements using onNodeWithText() or other selectors
  • Assert visibility and interactions using assertIsDisplayed() and performClick()

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.

Workflow: Running Tests in Android Studio

  1. Open the Gradle tool window
  2. Navigate to your module's test task
  3. Right-click and select Run Tests
  4. View results in the Run window

Was this article helpful?