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:
References
There are some articles have shown sharedTest
pattern, and they 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: