Using Robolectric in offline mode (a fix for Gradle parallel execution)

Using Robolectric in offline mode (a fix for Gradle parallel execution)

Recently, we enabled parallel execution on our Android project. With good performance gains in our build times came some flakiness from our Robolectric tests. We noticed roughly 20 percent (a completely anecdotal account) of the time, our tests would fail with a NoClassDefFound or NoSuchFieldError error.

Screen Shot 2022-07-13 at 7.17.56 PM.png

Searching for this issue on the Robolectric Github pages brings to light that others are experiencing a similar issue as well.

Example 1 Example 2 Example 3

One of the recommended approaches for dealing with this issue is using Robolectric in offline mode and either manually downloading the dependencies ahead of time or having Gradle download them at configuration time.

At the time of writing for Robolectric version 4.8.1, this was the approach we took to download the dependencies at configuration time.

In a file we will call `robolectric.gradle,' we can add the following code:

configurations {
    robo16Instrumented
    robo21
    robo21Instrumented
    robo28Instrumented
    robo29Instrumented
    robo31
    robo31Instrumented
    robo32Instrumented
}

dependencies {
    robo16Instrumented "org.robolectric:android-all-instrumented:4.1.2_r1-robolectric-r1-i4"
    robo21 "org.robolectric:android-all:5.0.2_r3-robolectric-r0"
    robo21Instrumented "org.robolectric:android-all-instrumented:5.0.2_r3-robolectric-r0-i4"
    robo28Instrumented "org.robolectric:android-all-instrumented:9-robolectric-4913185-2-i4"
    robo29Instrumented "org.robolectric:android-all-instrumented:10-robolectric-5803371-i4"
    robo31 "org.robolectric:android-all:12-robolectric-7732740"
    robo31Instrumented "org.robolectric:android-all-instrumented:12-robolectric-7732740-i4"
    robo32Instrumented "org.robolectric:android-all-instrumented:12.1-robolectric-8229987-i4"
}

def robolectricDependencies = "${rootProject.buildDir.path}/robolectric"

task fetchRobolectricDependencies(type: Copy) {
    from configurations.robo16Instrumented
    from configurations.robo21
    from configurations.robo21Instrumented
    from configurations.robo28Instrumented
    from configurations.robo29Instrumented
    from configurations.robo31
    from configurations.robo31Instrumented
    from configurations.robo32Instrumented
    into robolectricDependencies
}

subprojects {
    afterEvaluate {
        if (project.plugins.hasPlugin("com.android.application") || project.plugins.hasPlugin("com.android.library")) {
            android {
                testOptions.unitTests.all {
                    systemProperty 'robolectric.offline', 'true'
                    systemProperty 'robolectric.dependency.dir', robolectricDependencies
                }
            }

            tasks.withType(Test) {
                it.dependsOn fetchRobolectricDependencies
            }
        }
    }
}

and we will add that to our root build.gradle file via

apply from: "$project.rootDir/gradle/robolectric.gradle"

What this Gradle snippet is doing for us is adding the robolectric.offline property as well as the robolectric.dependency.dir property to point to the download dependencies to any project that has the application or library plugin (our main app module and the subsequent library modules that it depends on).

The exact dependencies Robolectric requires might vary for you when running in offline mode. The missing jar required by Robolectric will usually surface itself in a test failure looking something like:


java.lang.IllegalArgumentException: Path is not a file: /<build_dir>/robolectric/android-all-instrumented-12-robolectric-7732740-i4.jar

where the jar is the dependency that you should require in robolectric.gradle

With the Gradle script above, we were successfully able to add org.gradle.parallel=true without any flaky behavior from Robolectric.