16

I am wondering if there is anyway to stub the value of Build.Version.SDK_INT? Suppose I have the following lines in the ClassUnderTest:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
    //do work
}else{
    //do another work
}

How can I cover all the code ?

I mean I want to run two tests with different SDK_INT to enter both blocks.

Is it possible in android local unit tests using Mockito/PowerMockito?

Thanks

Evgeniy Mishustin
  • 3,343
  • 3
  • 42
  • 81

3 Answers3

33

Change the value using reflection.

 static void setFinalStatic(Field field, Object newValue) throws Exception {
    field.setAccessible(true);

    Field modifiersField = Field.class.getDeclaredField("modifiers");
    modifiersField.setAccessible(true);
    modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

    field.set(null, newValue);
 }

And then

 setFinalStatic(Build.VERSION.class.getField("SDK_INT"), 123);

It is tested. Works.

Update: There is a cleaner way to do it.

Create an interface

interface BuildVersionProvider {

    fun currentVersion(): Int

}

Implement the interface

class BuildVersionProviderImpl : BuildVersionProvider {

    override fun currentVersion() = Build.VERSION.SDK_INT

}

Inject this class as a constructor argument through the interface whenever you want current build version. Then in the tests when creating a SUT (System Under Test) object. You can implement the interface yourself. This way of doing things may be more code but follows the SOLID principles and gives you testable code without messing with reflection and system variables.

loshkin
  • 1,600
  • 2
  • 21
  • 38
  • That will not work because getField() expects for variable name and Build.VERSION.SDK_INT is actually a value. – Evgeniy Mishustin Jun 28 '16 at 11:07
  • 3
    setFinalStatic(Build.VERSION.class.getField("SDK_INT"), "123"); If this doesn't work maybe PowerMockito over mockito and mock the static field. – loshkin Jun 28 '16 at 11:30
  • amazing - used similar for Build.VERSION.CODENAME setFinalStatic(Build.VERSION.class.getField("CODENAME"), "DEBUG"); – galaxigirl Dec 21 '16 at 11:09
  • This worked perfectly for me. Just be sure to reset this value after you are finished testing. I am doing this in the @After method of my unit test so other classes are not affected. – fawaad Oct 19 '17 at 16:01
  • 1
    @toshkinl How did you manage to remove the precompiler error `Call requires Api level 26 (current min is: 24)`? It only vanishes, if I use `Build.VERSION.SDK_INT` directly. It remains, if I use a wrapper. – ASP Mar 09 '20 at 15:56
  • @ASP import java.lang.reflect.Field; import java.lang.reflect.Modifier; There is no Android code that requires any API levels. – loshkin Mar 09 '20 at 16:01
  • 1
    Doesn't work since Java 12 – DropDrage May 23 '23 at 18:20
12

As an alternative to reflection, you can use your own class that checks for API and then use Mockito to test the API-Dependent logic in fast JVM unit tests.

Example class

import android.os.Build

class SdkChecker {

    fun deviceIsOreoOrAbove(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O

}

Example tested method

fun createNotificationChannel(notificationManager: NotificationManager) {
    if (sdkChecker.deviceIsOreoOrAbove()) { // This sdkChecker will be mocked
        // If you target Android 8.0 (API level 26) you need a channel
        notificationManager.createNotificationChannel()
    }
}

Example unit tests

import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.verify
import com.nhaarman.mockito_kotlin.verifyZeroInteractions
import com.nhaarman.mockito_kotlin.whenever

@Test
fun createNotificationChannelOnOreoOrAbove() {
    whenever(mockSdkChecker.deviceIsOreoOrAbove()).thenReturn(true)

    testedClass.createNotificationChannel(mockNotificationManager)

    verify(mockNotificationManager).createNotificationChannel()
}

@Test
fun createNotificationChannelBelowOreo() {
    whenever(mockSdkChecker.deviceIsOreoOrAbove()).thenReturn(false)

    testedClass.createNotificationChannel(mockNotificationManager)

    verifyZeroInteractions(mockNotificationManager)
}
Sebastian
  • 2,896
  • 23
  • 36
3

The Solution with java 17, 2023.

Easy steps:

1.Adding the universal methods

private fun setStaticFieldViaReflection(field: Field, value: Any) {
    field.isAccessible = true
    getModifiersField().also {
        it.isAccessible = true
        it.set(field, field.modifiers and Modifier.FINAL.inv())
    }
    field.set(null, value)
}

private fun getModifiersField(): Field {
    return try {
        Field::class.java.getDeclaredField("modifiers")
    } catch (e: NoSuchFieldException) {
        try {
            val getDeclaredFields0: Method =
                Class::class.java.getDeclaredMethod(
                    "getDeclaredFields0",
                    Boolean::class.javaPrimitiveType
                )
            getDeclaredFields0.isAccessible = true
            val fields = getDeclaredFields0.invoke(Field::class.java, false) as Array<Field>
            for (field in fields) {
                if ("modifiers" == field.name) {
                    return field
                }
            }
        } catch (ex: ReflectiveOperationException) {
            e.addSuppressed(ex)
        }
        throw e
    }
}

2.Adding these flags to gradle (it can be changed for other cases)

android {
    ...

    testOptions {
        unitTests.all {
            jvmArgs(
                    "--add-opens", "java.base/java.lang=ALL-UNNAMED",
                    "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED"
            )
        }
    }
}

3.Examples of using

  private val testSocManufacturer = "test-soc-manufacturer"
  setStaticFieldViaReflection(
        Build::class.java.getDeclaredField("SOC_MANUFACTURER"),
        testSocManufacturer)

  private val testSocModel = "test-soc-model"
  setStaticFieldViaReflection(
        Build::class.java.getDeclaredField("SOC_MODEL"),
        testSocModel)

  setStaticFieldViaReflection(
        Build.VERSION::class.java.getDeclaredField("SDK_INT"),
        30)
Sergei S
  • 2,553
  • 27
  • 36