Stub value of Build.VERSION.SDK_INT in Local Unit Test - android

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

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.

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)
}

Related

Mocking of my companion object's function seems not to work for Unit tests and mockk library

I have a custom Logger used thorough the application. It is doing more, but it can be simplified as:
class MyLogger {
companion object {
fun d(tag: String, msg: String): Int {
return Log.d(tag, msg)
}
}
}
Again, for simplicity, asume a function like the following:
fun addWithLogger(val1: Int, val2: Int): Int {
MyLogger.d("TEST", "adding $val2 to $val1")
return (val1 + val2).also {
MyLogger.d("TEST", "result $it")
}
}
I am trying to test the function using JUnit, and mock the MyLogger.d function using mockk library:
#Test
fun additionWithLogger_isCorrect() {
val testClass = TestClass()
mockkStatic(MyLogger::class)
every { MyLogger.d(any(), any()) } returns 0
assertEquals(5, testClass.addWithLogger(2, 3))
}
I have tried with mockkStatic, mockkObject, with MyLogger.Companion etc, but all my attempts are resulting in something simillar:
Method d in android.util.Log not mocked. See http://g.co/androidstudio/not-mocked for details.
java.lang.RuntimeException: Method d in android.util.Log not mocked. See http://g.co/androidstudio/not-mocked for details.
at android.util.Log.d(Log.java)
at example.MyLogger$Companion.d(MyLogger.kt:8)
at example.ExampleUnitTest$additionWithLogger_isCorrect$1.invoke(ExampleUnitTest.kt:25)
at example.ExampleUnitTest$additionWithLogger_isCorrect$1.invoke(ExampleUnitTest.kt:25)
at io.mockk.impl.eval.RecordedBlockEvaluator$record$block$1.invoke(RecordedBlockEvaluator.kt:25)
at io.mockk.impl.eval.RecordedBlockEvaluator$enhanceWithRethrow$1.invoke(RecordedBlockEvaluator.kt:78)
at io.mockk.impl.recording.JvmAutoHinter.autoHint(JvmAutoHinter.kt:23)
at io.mockk.impl.eval.RecordedBlockEvaluator.record(RecordedBlockEvaluator.kt:40)
at io.mockk.impl.eval.EveryBlockEvaluator.every(EveryBlockEvaluator.kt:30)
at io.mockk.MockKDsl.internalEvery(API.kt:94)
at io.mockk.MockKKt.every(MockK.kt:143)
at example.ExampleUnitTest.additionWithLogger_isCorrect(ExampleUnitTest.kt:25)
...
It seems, that the MyLogger.d function is still being invoked, despite I am trying to mock it? What am I missing?
I have tried mockito and mockk libraries, but with no luck. I would like to test the functionality using Unit tests on PC. Tests are running ok as instrumental on Android device.
Code to be tested is already written, so I do not have much possibilities to adjust the MyLogger inside...

Method isDigitsOnly in android.text.TextUtils not mocked Error

I created a validation use case in which I'm validating the input using isDigitsOnly that use TextUtils internally.
override fun isDigitsOnly(size: String): Boolean {
return !size.trim().isDigitsOnly()
}
when I tried to test it, I got this error
Method isDigitsOnly in android.text.TextUtils not mocked
Does anyone know how I can mock the textUtils in my test class
#RunWith(MockitoJUnitRunner::class)
class ValidationInputImplTest {
#Mock
private lateinit var mMockTextUtils: TextUtils
private lateinit var validationInputImpl: ValidationInputImpl
#Before
fun setUp() {
validationInputImpl = ValidationInputImpl()
}
#Test
fun `contains only digits, returns success`() {
val input = "66"
val result = validationInputImpl(input)
assertTrue(result is ValidationResult.Success)
}
}
At the time of writing the answer,you cannot do that. Because the android code ,that you see is not actual code. You cannot create unit test for that. The default implementation of the methods in android.jar is to throw exception.
One thing you can do is, adding the below in build.gradle file.
But it make the android classes not to throw exception. But it will always return default value. So the test result may actually not work. It is strictly not recommended.
android {
testOptions {
unitTests.returnDefaultValues = true
}
}
The better way to do copy the code from android source and paste the file under src/test/java folder with package name as android.text .
Link to Answer

Mockito + Espresso + Dagger2 => ClassNotFoundException when running SDK is lower than target SDK

I came across this limitation when trying to mock a class that refers to classes present in SDK >= 26 and executed the test in a device running SDK 24.
I created a test app to better understand the problem.
open class RandomStuff{
#RequiresApi(Build.VERSION_CODES.O)
fun createDefaultNotificationChannel(): NotificationChannel {
return NotificationChannel("test", "test", NotificationManager.IMPORTANCE_DEFAULT)
}
fun testString(): String = "This is a test string"
}
Note that the RandomStuff class refers to the NotificationChannel class, only found in SDK >=26.
The test object provided via Dagger is the following:
#Module
class AppTestModule : AppModule() {
#Provides
override fun provideRandomStuff(): RandomStuff {
return mock(RandomStuff::class.java)
}
}
mock will fail with this exception:
java.lang.IllegalArgumentException: Could not create type
at provideRandomStuff(AppTestModule.kt) ultimately caused by a
ClassNotFoundException thrown by ByteBuddy (a component used by Mockito).
I was able to work around this issue by adding some checks to my provide method:
#Module
class AppTestModule : AppModule() {
#Provides
override fun provideRandomStuff(): RandomStuff? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return mock(RandomStuff::class.java)
} else {
return null
}
}
}
This temporary solution is very intrusive since now, the production code has to take into consideration that RandomStuff may be null.
I call this a limitation since Mockito has always been promoted as a unit testing framework it does not seem that the Dagger + Espresso + Mockito is a fully supported combination.
Is there a more creative solution for this issue?

How to test constraints on object initialization?

I am writing an App for Android, and I am wanting to start writing tests for the classes I am writing. I am fairly new to writing test cases.
Right now to create a test, I use IntelliJ and use it's wizard to make a new JUnit4 test. The wizard allows me to select methods from my class to test.
But for the object I am testing, I do not want a negative number passed to the constructor.
class MinuteTime(private val minutes : Int) {
init {
if (minutes < 0) {
throw IllegalArgumentException("Cannot be less than 0.")
}
}
...
Where in my Test class is the best place to test these constraints? I know that to test the constraints, I just need to make sure the exception is thrown and caught, but I am unsure if I should make a new method for this, or just wedge it into one of the functions IntelliJ pre-made for me. Here is the generated test class:
class MinuteTimeTest {
#Test
fun getTimeInMinutes() {
}
#Test
fun getHours() {
}
#Test
fun getMinutes() {
}
#Test
fun plus() {
}
}
I'd definetly recommend to wrap this into a separate test method. Its concern is simply testing the validation during initialization:
#Test
fun negativeNumberConstructorTest() {
assertFailsWith(IllegalArgumentException::class){
MinuteTime(-1)
}
}
It’s using kotlintest on top of JUnit

Ignoring Android unit tests depending on SDK level

Is there an annotation or some other convenient way to ignore junit tests for specific Android SDK versions? Is there something similar to the Lint annotation TargetApi(x)? Or do I manually have to check whether to run the test using the Build.VERSION?
I don't think there is something ready but it pretty easy to create a custom annotation for this.
Create your custom annotation
#Target( ElementType.METHOD )
#Retention( RetentionPolicy.RUNTIME)
public #interface TargetApi {
int value();
}
Ovverride the test runner (that will check the value and eventually ignore/fire the test)
public class ConditionalTestRunner extends BlockJUnit4ClassRunner {
public ConditionalTestRunner(Class klass) throws InitializationError {
super(klass);
}
#Override
public void runChild(FrameworkMethod method, RunNotifier notifier) {
TargetApi condition = method.getAnnotation(TargetApi.class);
if(condition.value() > 10) {
notifier.fireTestIgnored(describeChild(method));
} else {
super.runChild(method, notifier);
}
}
}
and mark your tests
#RunWith(ConditionalTestRunner.class)
public class TestClass {
#Test
#TargetApi(6)
public void testMethodThatRunsConditionally() {
System.out.print("Test me!");
}
}
Just tested, it works for me. :)
Credits to: Conditionally ignoring JUnit tests
An alternative is to use JUnit's assume functionality:
#Test
fun shouldWorkOnNewerDevices() {
assumeTrue(
"Can only run on API Level 23 or newer because of reasons",
Build.VERSION.SDK_INT >= 23
)
}
If applied, this effectively marks the test method as skipped.
This is not so nice like the annotation solution, but you also don't need a custom JUnit test runner.
I've been searching for an answer to this question, and haven't found a better way than to check the version. I was able to conditionally suppress the execution of test logic by putting a check in the following Android TestCase methods. However, this doesn't actually prevent the individual tests from executing. Overriding the runTest() method like this will cause tests to "pass" on API levels you know will not work. Depending on your test logic, you may want to override tearDown() too. Maybe someone will offer a better solution.
#Override
protected void setUp() throws Exception {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
if (Log.isLoggable(TAG, Log.INFO)) {
Log.i(TAG, "This feature is only supported on Android 2.3 and above");
}
} else {
super.setUp();
}
}
#Override
protected void runTest() throws Throwable {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
assertTrue(true);
} else {
super.runTest();
}
}
I think #mark.w's answer is the path of least resistance:
You might wanna take a look at SdkSuppress annotation. It has two methods -- maxSdkVersion and minSdkVersion which you could use depending on your need.
for example:
#Test
#SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
public void testMethodThatRunsConditionally() {
System.out.print("Test me!");
}
I have upgraded answer of #Enrichman for Kotlin modern version. So, this is test runner class:
class ConditionalSDKTestRunner(klass: Class<*>?) : BlockJUnit4ClassRunner(klass) {
override fun runChild(method: FrameworkMethod, notifier: RunNotifier) {
// Annotation class could not have a base class of interface, so we can not group them.
val testOnlyForTargetSDKCode = method.getAnnotation(TestOnlyForTargetSDK::class.java)?.sdkLevel?.code
val testForTargetSDKAndBelowCode = method.getAnnotation(TestForTargetSDKAndBelow::class.java)?.sdkLevel?.code
val testForTargetSDKAndAboveCode = method.getAnnotation(TestForTargetSDKAndAbove::class.java)?.sdkLevel?.code
when {
// If annotation exists, but target SDK is not equal of emulator SDK -> skip this test.
testOnlyForTargetSDKCode != null && testOnlyForTargetSDKCode != Build.VERSION.SDK_INT ->
notifier.fireTestIgnored(describeChild(method))
// If annotation exists, but test SDK is lower than emulator SDK -> skip this test.
testForTargetSDKAndBelowCode != null && testForTargetSDKAndBelowCode < Build.VERSION.SDK_INT ->
notifier.fireTestIgnored(describeChild(method))
// If annotation exists, but test SDK is higher than emulator SDK -> skip this test.
testForTargetSDKAndAboveCode != null && testForTargetSDKAndAboveCode > Build.VERSION.SDK_INT ->
notifier.fireTestIgnored(describeChild(method))
// For other cases test should be started.
else -> super.runChild(method, notifier)
}
}
}
Enum with exist SDKs in your project:
enum class SdkLevel(val code: Int) {
SDK_24(24),
SDK_25(25),
SDK_26(26),
SDK_27(27),
SDK_28(28),
SDK_29(29),
SDK_30(30),
SDK_31(31),
SDK_32(32)
}
and annotations below:
#Target(AnnotationTarget.FUNCTION)
#Retention(AnnotationRetention.RUNTIME)
annotation class TestForTargetSDKAndAbove(val sdkLevel: SdkLevel)
#Target(AnnotationTarget.FUNCTION)
#Retention(AnnotationRetention.RUNTIME)
annotation class TestForTargetSDKAndBelow(val sdkLevel: SdkLevel)
#Target(AnnotationTarget.FUNCTION)
#Retention(AnnotationRetention.RUNTIME)
annotation class TestOnlyForTargetSDK(val sdkLevel: SdkLevel)
To use it just add ConditionalSDKTestRunner to your base android unit test class:
#RunWith(ConditionalSDKTestRunner::class)
abstract class BaseAndroidUnitTest
and necessary annotation for test to make it actual only for special sdk:
#Test
#TestForTargetSDKAndAbove(SdkLevel.SDK_31)
fun getConnectionInfo_positive_SDK31() {
#Test
#TestForTargetSDKAndBelow(SdkLevel.SDK_30)
fun getConnectionInfo_negative1_SDK30() {
#Test
#TestOnlyForTargetSDK(SdkLevel.SDK_29)
fun getConnectionInfo_negative1_SDK29() {
That's all. Thanks!

Categories

Resources