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](http://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
. The 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 result more quickly. It can help developers to speed up TDD cycles:
References
There are some articles have shown sharedTest pattern, and they are should be mentioned here:
Sharing code between local and instrumentation tests by Alex Zhukovich
Powerful Approaches to Develop Shared Android Tests by Oleksandr Hrybuk
Sharing code between unit tests and instrumentation tests on Android by Dan Lew
There is an awesome book has introduced sharedTest pattern too:
There are some Google’s projects have used sharedTest pattern to sharing test code:
accompanist: [All] Share tests to run on Robolectric & Emulators by chrisbanes