Since Robolectric 4.0, we can also run Espresso-like tests also on the JVM with Robolectric. That is part of the Project nitrogen from Google (which became Unified Test Platform), where they want to allow developers to write UI test once, and run them everywhere.
However, earlier if you tried to run Kaspresso-like test extending TestCase on the JVM with Robolectric, you got the following error:
java.lang.NullPointerException
at androidx.test.uiautomator.QueryController.<init>(QueryController.java:95)
at androidx.test.uiautomator.UiDevice.<init>(UiDevice.java:109)
at androidx.test.uiautomator.UiDevice.getInstance(UiDevice.java:261)
at com.kaspersky.kaspresso.kaspresso.Kaspresso$Builder.<init>(Kaspresso.kt:297)
at com.kaspersky.kaspresso.kaspresso.Kaspresso$Builder$Companion.simple(Kaspresso.kt:215)
...
That is because Robolectric is just compatible with Espresso and not with uiAutomator.
Now, all Kaspresso tests are allowed to be executed correctly on the JVM with Robolectric with the following restrictions:
- Easy configuration of your project according to Robolectric guideline.
- Not possible to use adb-server because there is no a term like "Desktop" on the JVM environment. Tests that use adb-server will crash on the JVM with Robolectric with very explaining error message.
- Not possible to work with
UiDevice
andUiAutomation
classes. That's why a lot of (not all!) implementations inDevice
will crash on the JVM with Robolectric withNotSupportedInstrumentalTestException
. - Non working Kautomator. Mentioned problem with
UiDevice
andUiAutomation
classes affect the entire Kautomator. So, tests using Kautomator will crash on the JVM with Robolectric withKautomatorInUnitTestException
. - Interceptors that use
UiDevice
,UiAutomation
or adb-server are turning off on the JVM with Robolectric automatically. DocLocScreenshotTestCase
will crash on the JVM with Robolectric withDocLocInUnitTestException
.
To create a test that can run on a device/emulator and on the JVM, we recommend to create a sharedTest
folder, and configure sourceSets
in gradle accordingly, similar to what you can see under the build.gradle.kts
:samples:kaspresso-sample
sourceSets {
...
//configure shared test folder
val sharedTestFolder = "src/sharedTest/kotlin"
val androidTest by getting {
java.srcDirs("src/androidTest/java", sharedTestFolder )
}
val test by getting {
java.srcDirs("src/test/java", sharedTestFolder )
}
}
It is also important that such tests use @RunWith(AndroidJUnit4::class)
, since it is required by Robolectric.
In order to run your shared tests as Unit Tests on the JVM, you need to run a command looking like this:
./gradlew :MODULE:testVARIANTUnitTest --info --tests "PACKAGE.CLASS"
For example, to run the sample RobolectricTest on the JVM you need to run:
./gradlew :samples:kaspresso-sample:testDebugUnitTest --info --tests "com.kaspersky.kaspressample.sharedtest.SharedSimpleFlakyTest"
To run them on a device/emulator, the command to run would look like this:
./gradlew :MODULE:connectedVARIANTAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=PACKAGE.CLASS
For instance, to run the sample SharedTest on a device/emulator, you need to run:
./gradlew :samples:kaspresso-sample:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.kaspersky.kaspressample.sharedtest.SharedSimpleFlakyTest
We've prepared a bunch of tools and advices to accommodate your tests for the JVM (with Robolectric) environment.
Let's consider the most popular problem when a test uses a class containing calls to UiDevice
/UiAutomation
/AdbServer
or other not working in JVM environment things.
For example, your test looks like below:
@RunWith(AndroidJUnit4::class)
class FailingSharedTest : TestCase() {
@get:Rule
val runtimePermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
@get:Rule
val activityTestRule = ActivityTestRule(DeviceSampleActivity::class.java, false, true)
@Test
fun exploitSampleTest() =
run {
step("Press Home button") {
device.exploit.pressHome()
}
//...
}
}
device.exploit.pressHome()
calls UiDevice
under the hood and it leads to a crash the JVM environment.
There is following possible solution:
// change an implementation of Exploit class
@RunWith(AndroidJUnit4::class)
class FailingSharedTest : TestCase(
kaspressoBuilder = Kaspresso.Builder.simple {
exploit =
if (isAndroidRuntime) ExploitImpl() // old implementation
else ExploitUnit() // new implementation without UiDevice
}
) { ... }
// isAndroidRuntime property is available in Kaspresso.Builder.
Also, if your custom Interceptor uses UiDevice
/UiAutomation
/AdbServer
then you can turn off this Interceptor for JVM. The example:
class KaspressoConfiguringTest : TestCase(
kaspressoBuilder = Kaspresso.Builder.simple {
viewBehaviorInterceptors = if (isAndroidRuntime) mutableListOf(
YourCustomInterceptor()
FlakySafeViewBehaviorInterceptor(flakySafetyParams, libLogger)
) else mutableListOf(
FlakySafeViewBehaviorInterceptor(flakySafetyParams, libLogger)
)
}
) { ... }
Of course, there is a very obvious last option. Just don't include the test in a set of Unit tests.
Further remarks
As of Robolectric 4.5.1, there are some limitations to sharedTest: those tests run flawless on an emulator/device, but fail on the JVM
- Robolectric-Espresso does not support Idling resources yet
- Robolectric-Espresso will not support tests that start new activities(i.e. activity jumping)