19

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

6 Answers 6

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.

Sign up to request clarification or add additional context in comments.

7 Comments

That will not work because getField() expects for variable name and Build.VERSION.SDK_INT is actually a value.
setFinalStatic(Build.VERSION.class.getField("SDK_INT"), "123"); If this doesn't work maybe PowerMockito over mockito and mock the static field.
amazing - used similar for Build.VERSION.CODENAME setFinalStatic(Build.VERSION.class.getField("CODENAME"), "DEBUG");
@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.
Doesn't work since Java 12
|
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) } 

Comments

12

The newest solution with java 17 (I guess it helps from java 11 but didn't test it)

Just follow the steps:

1.Add the universal methods to your code

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) } 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.Add these flags to gradle (it can be changed for other java classes)

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

//SDK_INT setStaticFieldViaReflection(Build.VERSION::class.java.getDeclaredField("SDK_INT"), 30) //SOC_MANUFACTURER private val testSocManufacturer = "test-soc-manufacturer" setStaticFieldViaReflection(Build::class.java.getDeclaredField("SOC_MANUFACTURER"), testSocManufacturer) //SOC_MODEL private val testSocModel = "test-soc-model" setStaticFieldViaReflection(Build::class.java.getDeclaredField("SOC_MODEL"), testSocModel) 

Comments

2

Thanks Sergei for the JDK 17 solution. A more Kotlin-ish function for Step #1 of Sergei's answer:

private fun getModifiersField(): Field = try { Field::class.java.getDeclaredField("modifiers") } catch (e: NoSuchFieldException) { try { val getDeclaredFields0: Method = Class::class.java.getDeclaredMethod("getDeclaredFields0", Boolean::class.javaPrimitiveType) getDeclaredFields0.isAccessible = true @Suppress("unchecked_cast") (getDeclaredFields0.invoke(Field::class.java, false) as Array<Field>).first { it.name == "modifiers" } } catch (ex: ReflectiveOperationException) { e.addSuppressed(ex) throw e } } 

Comments

0

We recently came across this issue at work. We created a function and in our unit tests we just mocked it. Problem was that we lost the linter and on one release we had an issue that we never caught due to the linter. Anyways, we tried the reflection on JAVA 17 and it worked like a charm, but the moment we started testing all of the unit tests we had so many issues. For example, if 1 unit test that touched the reflection failed for whatever reason (even forcing an exception or Assert.fail("error")) then it caused a lot of random tests to fail. Then we thought, we could deal with that, but then another test was switched over and the reflection caused an issue with another mock and it failed no matter what. So after fighting with chatgpt for hours yesterday and today, we got a nice modified solution that works with the linter. We ended up doing something like this:

fun androidSdkVersionAtLeastQ() = Build.VERSION.SDK_INT > Build.VERSION_CODES.Q @RequiresApi(Build.VERSION_CODES.Q) fun qFun() { //api q } if (androidSdkVersionGreaterThan(Build.VERSION_CODES.Q)) { qFun() } 

So basically for each API that each part of the code requires you create a function for it and just mock it in your unit test

I hope this helps anybody. the last 2 days were hell, but we had been fighting with this for months, at least a year, on a good solution and now this is just perfect.

Comments

0

You could use

@Config(sdk = [31]) 

from Robolectric

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.