Testing Composables with Robolectric
Back in Jetpack Compose 1.0.0-beta09, it was announced that Compose tests could be run on Robolectric. After looking through the official documentation however, I wasn’t able to find an explanation of how to set it up. With some trial and error, I got it all to work, and I’m writing this down for anyone else who wants to do the same, or runs into the same problems I did. The code written here works with Compose 1.0.1, Robolectric 4.6.1, and AGP 7.0.1.
If you just want to see how to make it work, and not all the problems you may run into, skip to the end.
Getting ComposeTestRule
to work
To start off,
I added the dependencies specified in the official documentation to my project.
It didn’t make sense to use the androidTestImplementation
configuration though,
since I wanted the tests to run on a local JVM.
So instead I used testImplementation
and specified the dependencies like:
testImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")
After that, I created a single JUnit 4 test to check the setup:
class ComposeTest {
@get:Rule val rule = createComposeRule()
@Test
fun emptyTest() {
}
}
Without the Rule
,
emptyTest
would pass because it does nothing.
But as-is,
the test failed with the following exception:
java.lang.ExceptionInInitializerError
at androidx.test.ext.junit.rules.ActivityScenarioRule.lambda$new$0$ActivityScenarioRule(ActivityScenarioRule.java:70)
...
at androidx.compose.ui.test.junit4.android.ComposeRootRegistry$getStatementFor$1.evaluate(ComposeRootRegistry.android.kt:150)
...
Caused by: java.lang.RuntimeException: Method allowThreadDiskReads in android.os.StrictMode not mocked. See http://g.co/androidstudio/not-mocked for details.
at android.os.StrictMode.allowThreadDiskReads(StrictMode.java)
at androidx.test.internal.platform.ServiceLoaderWrapper.loadService(ServiceLoaderWrapper.java:42)
at androidx.test.internal.util.Checks.<clinit>(Checks.java:132)
... 52 more
The solution to this problem is included in the error message.
I was using Gradle’s Kotlin DSL,
so I added isReturnDefaultValues = true
to my build.gradle.kts
:
android {
// ...
testOptions {
unitTests {
isReturnDefaultValues = true
}
}
}
I then re-ran the tests,
and saw another Exception
thrown:
java.lang.IllegalStateException: No instrumentation registered! Must run under a registering instrumentation.
at androidx.test.platform.app.InstrumentationRegistry.getInstrumentation(InstrumentationRegistry.java:45)
...
at androidx.compose.ui.test.junit4.android.ComposeRootRegistry$getStatementFor$1.evaluate(ComposeRootRegistry.android.kt:150)
...
This makes sense as the instrumentation is normally provided at runtime when running on a real Android OS.
I wanted to use Robolectric though,
and didn’t specify that yet.
Annotating the test suite with @RunWith(RobolectricTestRunner::class)
fixes this problem.
But after that,
I ended up running into another problem:
No such manifest file: ./AndroidManifest.xml
Unable to resolve activity for Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=org.robolectric.default/androidx.activity.ComponentActivity } -- see https://github.com/robolectric/robolectric/pull/4736 for details
java.lang.RuntimeException: Unable to resolve activity for Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=org.robolectric.default/androidx.activity.ComponentActivity } -- see https://github.com/robolectric/robolectric/pull/4736 for details
at org.robolectric.android.internal.RoboMonitoringInstrumentation.startActivitySyncInternal(RoboMonitoringInstrumentation.java:84)
...
This error meant that the manifest from ui-test-manifest
wasn’t being found,
and the issue mentioned in this error message doesn’t quite help in this case.
What I needed to do is ensure that my tests had access to Android resources by setting isIncludeAndroidResources
to true.
With all that, the empty test finally passed.
Getting a Compose test to work
Now I went to write a test that actually used Compose by using ComposeContentTestRule.setContent
:
@Test
fun textIsDisplayed() {
rule.setContent {
Text("Testing Compose")
}
rule.onNodeWithText("Testing Compose").assertIsDisplayed()
}
This time,
I ran into an IllegalAccessException
when ModernAsyncTask
was being accessed:
java.util.concurrent.ExecutionException: java.lang.RuntimeException: java.lang.IllegalAccessException: class androidx.test.espresso.base.ThreadPoolExecutorExtractor$2 cannot access a member of class androidx.loader.content.ModernAsyncTask with modifiers "public static final"
java.lang.RuntimeException: java.util.concurrent.ExecutionException: java.lang.RuntimeException: java.lang.IllegalAccessException: class androidx.test.espresso.base.ThreadPoolExecutorExtractor$2 cannot access a member of class androidx.loader.content.ModernAsyncTask with modifiers "public static final"
...
at androidx.compose.ui.test.junit4.AndroidComposeTestRule.waitForIdle(AndroidComposeTestRule.android.kt:286)
at androidx.compose.ui.test.junit4.AndroidComposeTestRule.setContent(AndroidComposeTestRule.android.kt:281)
at io.github.rsookram.example.ComposeTest.emptyTest(ComposeTest.kt:17)
...
Searching for that error in Robolectric’s Github repository turned up this issue.
The workaround for now is to have Robolectric instrument the androidx.loader.content
package.
This can be done by setting the instrumentedPackages
property when configuring Robolectric.
I configured Robolectric using @Config
,
and with that,
I was finally able to run a Compose test using Robolectric.
The final result looked like this:
@Config(instrumentedPackages = ["androidx.loader.content"])
@RunWith(RobolectricTestRunner::class)
class ComposeTest {
@get:Rule val rule = createComposeRule()
@Test
fun textIsDisplayed() {
rule.setContent {
Text("Testing Compose")
}
rule.onNodeWithText("Testing Compose").assertIsDisplayed()
}
}
TL;DR
The next time I need to set up Robolectric for Compose in a project, these are the steps that I’ll take to make sure everything works:
- Add the dependencies to the project:
androidx.compose.ui:ui-test-junit4
and Robolectric under thetestImplementation
configurationandroidx.compose.ui:ui-test-manifest
under thedebugImplementation
configuration
- Configure
testOptions
in the Gradle configuration by settingisReturnDefaultValues
andisIncludeAndroidResources
totrue
. - Run the test suite using Robolectric by setting the test runner with
@RunWith
. - Instrument the
androidx.loader.content
package with@Config
orrobolectric.properties
.
If you want to see how this all comes together, take a look at this PR where I added Compose tests to the SRS app I’m working on.