Mobile Development 12 min read

How to Build Reliable Android Automated Tests: From Unit to UI

This article explains why Android testing is crucial, outlines common testability problems, and provides step‑by‑step solutions using MVP architecture, interface‑based design, JUnit, Mockito, Robolectric, Espresso, and CI integration to create maintainable unit and UI tests.

Huajiao Technology
Huajiao Technology
Huajiao Technology
How to Build Reliable Android Automated Tests: From Unit to UI

Testing is a critical part of Android development. Early on, most tests are manual, but as app complexity and release speed increase, manual testing becomes a bottleneck. In early 2019, the Huajiao Android team began exploring automated testing and documented the challenges and solutions they encountered.

Improving Testability

The main obstacles were high code coupling (most code resides in Activities/Fragments) and complex class dependencies that rely on concrete implementations, making it hard to create stable test environments.

Extract business logic using MVP and a clean Architecture, moving pure logic into simple Java/Kotlin classes.

Depend on interfaces rather than concrete classes, allowing easy substitution with mock implementations.

Eliminate hidden dependencies: avoid creating new objects inside methods; instead, abstract object creation behind interfaces and inject them via constructors.

Unit Testing

Unit tests catch bugs early and improve code design, encouraging high cohesion and low coupling. The primary tool is JUnit.

Add the JUnit dependency: testImplementation 'junit:junit:4.12' Place test code under src/test/java (or src/test/kotlin for Kotlin). Configure source sets in build.gradle:

sourceSets {
    main {
        java {
            srcDirs = ['src/main/java', 'src/main/kotlin']
        }
    }
    androidTest {
        java {
            srcDirs = ['src/test/java', 'src/test/kotlin', 'src/androidTest/java', 'src/androidTest/kotlin']
        }
    }
}

Typical unit‑test workflow with JUnit:

Create the object under test, keeping constructors simple and fully initializing the object.

Invoke a single method that should produce only one kind of effect (return value, state change, or interaction with a dependency).

Use assert statements to verify the expected outcome. For state changes, expose getters; for interactions, use Mockito to mock dependencies and verify calls.

Mockito

Mockito helps mock irrelevant dependencies, provide stable behavior, and verify interactions.

// Example using annotations
@RunWith(org.mockito.junit.MockitoJUnitRunner::class)
class SomeTester {
    @get:Rule
    var rule = MockitoJUnit.rule()
}

class SomeTester {
    fun test() {
        // Directly create a mock object
        val author: Bean = Mockito.mock(Bean::class.java)
    }
}

Robolectric

Standard unit tests run on the JVM and lack a real Android runtime, leading to RuntimeException("Stub!") errors because android.jar is only a stub. Robolectric provides a local Android sandbox that implements the Android API, allowing tests to run with RobolectricTestRunner without those exceptions. However, unit tests should focus on application logic, leaving platform‑specific tests to integration testing with frameworks like Espresso.

Test Reports and CI Integration

Automated tests can be executed repeatedly, making them ideal for CI pipelines. In Jenkins, configure a Gradle test task. If the project uses product flavors, use the corresponding flavor task name. After the test task finishes, reports are generated under /build/test-results/testDebugUnitTest/ and can be published via Jenkins' "Publish JUnit test report" feature.

When multiple flavors exist, ensure the correct flavor’s test task is used. Jenkins then displays the test results alongside the build.

Integration Testing

Integration tests reside in src/androidTest and share the same package structure as the main source. When run, Android Studio builds two APKs: the regular app APK and a test APK containing assets, resources, and a separate AndroidManifest.xml. Execute tests with:

adb shell am instrument -w <test_package_name>/<runner_class>

Typical values: test_package_name is the app’s package, and runner_class is often androidx.test.runner.AndroidJUnitRunner. Within integration tests you can access both the test APK’s and the app APK’s resources via InstrumentationRegistry.getContext() and InstrumentationRegistry.getTargetContext(). Corresponding R classes are com.xxx.test.R and com.xxx.R.

Espresso UI Testing

Espresso, Google’s UI testing framework, lets tests interact with the app from a user’s perspective.

Launch the activity using ActivityTestRule (or a custom rule).

// Launch MainActivity
@get:Rule
val activityRule = ActivityTestRule(MainActivity::class.java)

class DemoRule : ActivityTestRule<FragmentDemoActivity>(FragmentDemoActivity::class.java) {
    override fun beforeActivityLaunched() { /* prepare resources */ }
    override fun getActivityIntent(): Intent { /* customize intent */ }
    override fun afterActivityFinished() { /* cleanup */ }
}

Find a view and perform actions: onView(withId(R.id.group_money)) Espresso’s built‑in matchers lack direct support for RecyclerView child views, so Huajiao created a custom RecyclerViewMatcher:

public boolean matchesSafely(View view) {
    this.resources = view.getResources();
    if (childView == null) {
        RecyclerView recyclerView = (RecyclerView) view.getRootView().findViewById(recyclerViewId);
        if (recyclerView != null && recyclerView.getId() == recyclerViewId) {
            childView = recyclerView.findViewHolderForAdapterPosition(position).itemView;
        } else {
            return false;
        }
    }
    if (targetViewId == -1) {
        return view == childView;
    } else {
        View targetView = childView.findViewById(targetViewId);
        return view == targetView;
    }
}

After locating a view, perform actions (click, double‑click, swipe, etc.) and verify results using ViewInteraction.check with matchers like isDisplayed(), withText(), or custom assertions combined with JUnit asserts.

Conclusion

Android automated testing has historically been overlooked due to tool maturity, rapid app iteration, and learning costs. As app complexity grows and testing frameworks mature, more companies invest in automation. This article shared Huajiao’s practical experience with unit, UI, and integration testing, and future work will expand coverage and adopt additional testing strategies.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

AndroidAutomated Testingunit testingintegration testingCIMockitoEspressoRobolectric
Huajiao Technology
Written by

Huajiao Technology

The Huajiao Technology channel shares the latest Huajiao app tech on an irregular basis, offering a learning and exchange platform for tech enthusiasts.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.