Skip to content

Conditional Test Execution

Marcel Schnelle edited this page Jul 31, 2021 · 10 revisions

One of the most exciting features of JUnit 5 is the functionality provided by its Conditional Test Execution annotations. At runtime, JUnit Jupiter evaluates external properties in order to make a decision about whether or not an annotated test method should be executed. This allows developers to write specific tests that only run on certain Java versions or operating systems, or depend on certain environment variables or system properties, for example. This guide introduces those annotations in the context of the Android ecosystem.

Android-specific annotations

Since version 1.2.0, the android-test-core library includes a couple of annotations that drive conditional test execution in scenarios tailored towards Android developers. The following sections introduce these annotations and their effect on the execution of instrumentation tests.

@EnabledOnManufacturer/@DisabledOnManufacturer

Skip or drive execution for manufacturer-specific test cases. This helps with testing bugs scoped towards a certain subset of devices. For example, you can run a test only on Samsung devices, or skip it if the test suite is executed on a Huawei device.

// Matches e.g. "Samsung", "SAMSUNG", "sAmSuNg" if ignoreCase == true (default),
// and "Samsung" only if ignoreCase == false
@EnabledOnManufacturer(value = "Samsung", ignoreCase = true)
@Test
fun testForSamsungDevicesOnly() {
}

// Matches e.g. "Google", "GOOGLE", "gOoGlE" if ignoreCase == true (default),
// and "Google" only if ignoreCase == false
@DisabledOnManufacturer(value = "Google", ignoreCase = true)
@Test
fun testForAllButGoogleDevices() {
}

If one of these annotations causes a test to be skipped, you can find the following line in the Logcat output:

W/AndroidJUnit5: testForSamsungDevicesOnly is ignored. Disabled on Manufacturer: OnePlus

@EnabledOnSdkVersion/@DisabledOnSdkVersion

Skip or drive execution on specific API levels. This helps with testing apparent behavior on a certain range of OS versions. For example, you can run a test only on devices running Android P or newer, or skip it if the test suite is executed on a device older than Q.

// There is also an "until" parameter, indicating the highest API level where the test should be enabled
@EnabledOnSdkVersion(from = 28)
@Test
fun testRunningOnPieAndAbove() {
}

// There is also a "from" parameter, indicating the lowest API level where the test should be disabled
@DisabledOnSdkVersion(until = 28)
@Test
fun testNeverRunningOnPieAndBelow() {
}

If one of these annotations causes a test to be skipped, you can find the following line in the Logcat output:

W/AndroidJUnit5: testRunningOnPieAndAbove is ignored. Disabled on API 27

@EnabledIfBuildConfigValue/@DisabledIfBuildConfigValue

Skip or drive execution based on specific BuildConfig fields. When defining certain features through flags located in the build config, or testing flavor-specific logic, these annotations may come in handy. It's important to note that while BuildConfig fields can be of arbitrary type, these annotations will convert them into strings before matching against a regular expression.

// This test will be executed if the field "MY_KEY" contains four characters, e.g. when specifying:
// android.defaultConfig.buildConfigField("String", "MY_KEY", "\"ABCD\"")
@EnabledIfBuildConfigValue(named = "MY_KEY", matches = "\\w{4}")
@Test
fun testRunningWhenMyKeyHasFourCharacters() {
}

// This test will be skipped if the field "MY_INT", converted to string, equals "100", e.g. when specifying:
// android.defaultConfig.buildConfigField("int", "MY_INT", "100")
@DisabledIfBuildConfigValue(named = "MY_INT", matches = "100")
@Test
fun testSkippingWhenMyIntIsOneHundred() {
}

If one of these annotations causes a test to be skipped, you can find one of the following lines in the Logcat output:

testRunningWhenMyKeyHasFourCharacters is ignored. BuildConfig key [MY_KEY] with value [13370815] does not match regular expression [\\w{4}]
testSkippingWhenMyIntIsOneHundred is ignored. BuildConfig key [MY_VALUE] with value [100] matches regular expression [100]
testSomethingEntirelyDifferent is ignored. BuildConfig key [NON_EXISTENT_KEY] does not exist

Considerations for JVM-based annotations

The JUnit Jupiter API comes with its own set of annotations for conditional test execution. In the daily workflow of an Android developer, some of these yield more benefit than others. This section describes some considerations for using them efficiently.

@EnabledOnOs/@DisabledOnOs ⚠️

Android instrumentation tests will likely run only on Android devices. Therefore, this annotation pair doesn't really have much benefit. In fact, an Android device will match the OS.LINUX value, causing tests annotated with @EnabledOnOs(OS.LINUX) to always run, and tests annotated with @DisabledOnOs(OS.LINUX) to always be skipped. Other values of the enum will never match.

@EnabledOnJre/@DisabledOnJre

These annotations look up the value of the java.version system property, as well as some internals of the java.lang.Runtime class. On Android, these values are unset, causing a reported JRE version of 0 at all times. Do not rely on these annotations for Android - if you want to restrict execution based on the system, @EnabledOnSdkVersion and @DisabledOnSdkVersion are a better choice.

@EnabledIfEnvironmentVariable/@DisabledIfEnvironmentVariable

It isn't possible out of the box to provide environment variables to the Android instrumentation, outside of tools like gcloud and Firebase Test Lab. The JUnit 5 runner provides a handy feature for advanced usage to developers, allowing them to utilize @EnabledIfEnvironmentVariable and @DisabledIfEnvironmentVariable just like with JVM tests. You can provide them as a comma-separated value to the test runner in your build.gradle setup, as shown below:

android {
    defaultConfig {
        // Right below where you specify the Test Runner & JUnit 5 itself...
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        testInstrumentationRunnerArgument("runnerBuilder", "de.mannodermaus.junit5.AndroidJUnit5Builder")

        // ...you can provide this argument, too!
        testInstrumentationRunnerArgument("environmentVariables", "KEY1=value1,KEY2=value2")
    }
}

The environmentVariables key is read in and processed by the instrumentation runtime. If you need to provide multiple variables, please make sure to provide this argument only once, and separate different arguments with commas.

Using the above setup, the following behavior works as described in the comments:

@EnabledIfEnvironmentVariable(named = "KEY1", matches = "value1")
@Test
fun testSomething() {
    // This will be executed, because the variable exists with a matching value
}

@DisabledIfEnvironmentVariable(named = "KEY2", matches = "value2")
@Test
fun skipSomething() {
    // This will not be executed, because the variable exists with a matching value
}

@EnabledIfSystemProperty/@DisabledIfSystemProperty

Support for JUnit 5's system-property-based annotations is possible in a similar fashion to the environment variables.You can provide them as a comma-separated value to the test runner in your build.gradle setup, as shown below:

android {
    defaultConfig {
        // Right below where you specify the Test Runner & JUnit 5 itself...
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        testInstrumentationRunnerArgument("runnerBuilder", "de.mannodermaus.junit5.AndroidJUnit5Builder")

        // ...you can provide this argument, too!
        testInstrumentationRunnerArgument("systemProperties", "KEY1=value1,KEY2=value2")
    }
}

The systemProperties key is read in and processed by the instrumentation runtime. If you need to provide multiple variables, please make sure to provide this argument only once, and separate different arguments with commas.

Using the above setup, the following behavior works as described in the comments:

@EnabledIfSystemProperty(named = "KEY1", matches = "value1")
@Test
fun testSomething() {
    // This will be executed, because the property exists with a matching value
}

@DisabledIfSystemProperty(named = "KEY2", matches = "value2")
@Test
fun skipSomething() {
    // This will not be executed, because the variable exists with a matching value
}