Android SharedPreferences Tutorial and Example

Testing an Android screen with JUnit and Espresso

A basic UI test checks whether a real Android screen behaves as expected when a user taps, types, or selects something. JUnit provides the test structure and assertions, while Espresso drives views inside an Android application and verifies the result on the screen.

This approach is useful for beginner projects such as login forms, settings pages, SQLite list screens, and SharedPreferences examples. A small test can catch a broken button or incorrect label before the app reaches users in Sydney, Melbourne, Brisbane, or elsewhere across Australia.

How JUnit and Espresso work together

JUnit is the testing framework used to organise test methods. It provides annotations such as @Test, lifecycle methods, and assertions including assertEquals and assertTrue. In an Android project, JUnit can be used for local unit tests that run on the JVM.

Espresso is designed for instrumented UI tests. These tests run on an emulator or physical Android device because they need access to the application’s views, activity lifecycle, and Android framework. Espresso waits for the interface to become idle before performing actions, reducing timing problems in straightforward screens.

A test commonly follows three steps: locate a view, perform an action, and check the result. For example, it can find an EditText by ID, type a name, tap a button, and confirm that a TextView displays a greeting.

Preparing the Android project

In an Android Studio project using Gradle, add the AndroidX test libraries to the app module. Versions change over time, so use the current compatible versions recommended by Android Studio or the Android documentation.

dependencies {
    testImplementation("junit:junit:4.13.2")

    androidTestImplementation("androidx.test.ext:junit:1.2.1")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
}

The testImplementation dependency is for local JVM tests. The androidTestImplementation dependencies are for tests stored in src/androidTest, where Espresso tests normally belong. Check that the project uses an Android test runner, usually AndroidJUnitRunner.

When testing Australian users, avoid depending on one fixed device size. A commuter checking an app on a smaller phone in a Melbourne tram may see a different layout from someone using a large tablet at home. Use resource IDs rather than screen coordinates, and test strings through resources so locale-specific text such as en-AU can be handled safely.

Building a small screen to test

Consider a screen containing an input field, a button, and a result label:

<EditText
    android:id="@+id/nameInput"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="@string/name_hint" />

<Button
    android:id="@+id/greetButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/greet" />

<TextView
    android:id="@+id/resultText"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

The activity can read the input and update the label after a tap:

binding.greetButton.setOnClickListener {
    val name = binding.nameInput.text.toString()
    binding.resultText.text = "Hello, $name!"
}

Stable IDs are important because Espresso uses them to find views. Text matching can be useful for visible labels, but IDs are usually less fragile when copy changes or when an app supports several languages. A test should verify user-visible behaviour, not the private implementation details of the activity.

Writing the first Espresso test

Create a Kotlin file under app/src/androidTest/java/.... The test below launches MainActivity, enters a name, taps the button, and checks the displayed greeting:

@RunWith(AndroidJUnit4::class)
class MainActivityTest {

    @get:Rule
    val activityRule = ActivityScenarioRule(MainActivity::class.java)

    @Test
    fun enteringName_displaysGreeting() {
        onView(withId(R.id.nameInput))
            .perform(typeText("Alex"), closeSoftKeyboard())

        onView(withId(R.id.greetButton))
            .perform(click())

        onView(withId(R.id.resultText))
            .check(matches(withText("Hello, Alex!")))
    }
}

Import the required classes:

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.ext.junit.rules.ActivityScenarioRule
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.*

withId locates a view, typeText simulates keyboard input, click performs a tap, and matches makes the assertion. Closing the keyboard is helpful because the software keyboard can cover the button on a compact emulator.

For a quick reference when an Android API or matcher is unfamiliar, these Android testing notes can sit alongside the official documentation during development.

Running and diagnosing the test

Select the instrumented test in Android Studio and press the green run icon. Android Studio builds the test APK, installs it on the selected emulator or device, starts the activity, and reports whether each assertion passed. You can also run all instrumented tests with the Gradle task:

./gradlew connectedAndroidTest

A failed test should be read as a useful signal rather than simply a red result. “No views in hierarchy” often means the wrong ID, an activity that has not launched, or content that is not currently visible. A mismatch in text may indicate whitespace, a string resource difference, or an unexpected locale.

Tests involving network calls, animations, or delayed database work need extra care. Espresso handles many UI-idle situations, but custom background operations may require an idling resource or a fake repository. For a notification screen built around Firebase Cloud Messaging, a separate Firebase notification guide can help establish the feature before its UI states are tested.

Making UI tests reliable

Keep each test focused on one user outcome. A test for an empty form might verify that a validation message appears, while another test confirms that valid input produces a result. Short tests make failures easier to locate and reduce the temptation to ignore an unstable test suite.

Use accessibility-friendly selectors where possible. A visible button with a meaningful content description helps Espresso and improves support for TalkBack users. Avoid selectors based on exact screen positions, generated view indexes, or long chains of parent-child relationships.

A basic suite might compare these test types:

Test type Runs on Best use Typical example
Local JUnit test JVM Business logic without Android UI Validate a name formatter
Espresso UI test Emulator or device User interactions and visible results Tap a button and check a label
Repository test JVM or instrumented environment Database or data-access behaviour Insert and retrieve a SQLite record
End-to-end test Device with configured services Several app layers together Sign in and load a saved list

For Australian release testing, include a few practical checks around en-AU formatting, accessibility, and different screen densities. A list that works on a developer’s workstation may still be awkward during a rushed arvo commute or on a slower connection outside a capital city. Reliable selectors and isolated test data keep those checks repeatable in local development and continuous integration.