Skip to content

sharedTest pattern: sharing tests and speeding up development

After Robolectric's 4.0 release, Robolectric supports the AndroidJUnit4 test runner, ActivityScenario, and Espresso for interacting with UI components. As we know, we also can run those tests with an official emulator. This article will show an often overlooked but widely-used pattern called sharedTest to share tests between local and instrumentation tests. This will provide the benefit of fast unit testing while ensuring that tests are high-fidelity by enabling them to be run in an emulator.

Using sharedTest steps by steps

The first thing that sharedTest needs is AndroidJUnit4 test runner. It is a test runner that supports both Robolectric and androidx.test. There is a sample class, called SampleFragmentTest.kt from FragmentScenarioSample that uses AndroidJUnit4 test runner:

import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4

import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.LooperMode

/**
 * A test using the androidx.test unified API, which can execute on an Android device or locally using Robolectric.
 *
 * See [testing documentation](https://d.android.com/tools/testing).
 */
@RunWith(AndroidJUnit4::class)
class SampleFragmentTest {
    @Test
    fun launchFragmentAndVerifyUI() {
        // use launchInContainer to launch the fragment with UI
        launchFragmentInContainer<SampleFragment>()

        // now use espresso to look for the fragment's text view and verify it is displayed
        onView(withId(R.id.textView)).check(matches(withText("I am a fragment")));
    }
}

The second thing to enable sharedTest is to create a directory called sharedTest, at the same directory level with test and androidTest. Android Studio doesn't support it, so we should create it manually. FragmentScenarioSample's sharedTest directory is a good example for it.

The next step we should do is to add sharedTest directory to test's and androidTest's source directory. FragmentScenarioSample's build.gradle is also a good example for it:

// share the unified tests
sourceSets {
    test {
        java.srcDir 'src/sharedTest/java'
    }
    androidTest {
        java.srcDir 'src/sharedTest/java'
    }
}

If you want to share resources too, you can check Robolectric's PR: Add ctesque common tests to android test that used to reuse tests to improve CI fidelity with sharedTest pattern:

sourceSets {
    String sharedTestDir = 'src/sharedTest/'
    String sharedTestSourceDir = sharedTestDir + 'java'
    String sharedTestResourceDir = sharedTestDir + 'resources'
    test.resources.srcDirs += sharedTestResourceDir
    test.java.srcDirs += sharedTestSourceDir
    androidTest.resources.srcDirs += sharedTestResourceDir
    androidTest.java.srcDirs += sharedTestSourceDir
}

The last thing is to test it with ./gradlew test for local tests on Robolectric and ./gradlew connectedCheck for instrumentation tests on Emulator.

Why AndroidJUnit4 test runner?

There is an aspirational long-term goal for Android tests, write once, run everywhere tests on Android. The AndroidJUnit4 test runner is selected as the bridge for different devices that used to run tests. We can check AndroidJUnit4#getRunnerClassName(), and we can find how AndroidJUnit4 to delegate tests to real test runner based on running environment:

private static String getRunnerClassName() {
  String runnerClassName = System.getProperty("android.junit.runner", null);
  if (runnerClassName == null) {
    if (!System.getProperty("java.runtime.name").toLowerCase().contains("android")
        && hasClass("org.robolectric.RobolectricTestRunner")) {
      return "org.robolectric.RobolectricTestRunner";
    } else {
      return "androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner";
    }
  }
  return runnerClassName;
}

If it finds current running environment has RobolectricTestRunner, it will delegate tests to Robolectric's RobolectricTestRunner; otherwise to androidx.test's AndroidJUnit4ClassRunner.

Not only sharing code, but also speeding up development

With sharedTest pattern, we can share test code as much as possible. Is it the only benefit to encourage you to use sharedTest pattern? Not yet. Actually, Robolectric is a simulated Android environment inside a JVM. It has better speed to establish and destroy tests environment, and developers can get test results more quickly. It can help developers to speed up TDD cycles:

The two cycles associated with iterative, test-driven development

References

There are some articles have shown sharedTest pattern, and they should be mentioned here:

There is an awesome book has introduced sharedTest pattern too:

There are some Google's projects have used sharedTest pattern to sharing test code: