Search code examples
androidkotlinunit-testingandroid-intent

Problems testing values saved in android Intent (extras always null)


I have the following Android Intent extension:

import android.content.Intent
import android.os.Parcelable
import java.io.Serializable

fun Intent.addNewTaskFlags(): Intent = apply {
    addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_ANIMATION)
}

fun Intent.putData(dataMap: Map<String, Any?>): Intent = apply {
    dataMap.forEach { (key, data) ->
        data?.let {
            put(key, it)
        }
    }
}

private fun Intent.put(key: String, value: Any) {
    when (value) {
        is String -> putExtra(key, value)
        is Int -> putExtra(key, value)
        is Boolean -> putExtra(key, value)
        is Long -> putExtra(key, value)
        is Double -> putExtra(key, value)
        is Parcelable -> putExtra(key, value)
        is Serializable -> putExtra(key, value)
        else -> throw IllegalArgumentException("Incompatible data type: ${value.javaClass}")
    }
}

And the following unit test:

class IntentExtensionTest {

    @Test
    fun testAddNewTaskFlags() {
        // When
        val mockIntent = spyk(Intent())
        mockIntent.addNewTaskFlags()

        // Then
        verify {
            mockIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_ANIMATION)
        }
    }

    @Test
    fun testPutData() {
        // Given
        val parcelableValue = mockk<Parcelable>()
        val serializableValue = mockk<Serializable>()

        // When
        val intent = Intent().putData(
            mapOf(
                STRING_KEY to STRING_VALUE,
                INT_KEY to INT_VALUE,
                BOOLEAN_KEY to BOOLEAN_VALUE,
                LONG_KEY to LONG_VALUE,
                DOUBLE_KEY to DOUBLE_VALUE,
                PARCELABLE_KEY to parcelableValue,
                SERIALIZABLE_KEY to serializableValue
            )
        )

        // Then
        intent.run {
            val extras = extras ?: throw AssertionError("Intent extras are null")

            assertEquals(STRING_VALUE, extras.getString(STRING_KEY))
            assertEquals(INT_VALUE, extras.getInt(INT_KEY, -1))
            assertEquals(BOOLEAN_VALUE, extras.getBoolean(BOOLEAN_KEY, false))
            assertEquals(LONG_VALUE, extras.getLong(LONG_KEY, -1L))
            assertEquals(DOUBLE_VALUE, extras.getDouble(DOUBLE_KEY, -1.0))
            assertEquals(parcelableValue, extras.getParcelable(PARCELABLE_KEY))
            assertEquals(serializableValue, extras.getSerializable(SERIALIZABLE_KEY))
        }
    }

    companion object {
        private const val STRING_KEY = "stringKey"
        private const val STRING_VALUE = "Hello, World!"
        private const val INT_KEY = "intKey"
        private const val INT_VALUE = 1
        private const val BOOLEAN_KEY = "booleanKey"
        private const val BOOLEAN_VALUE = true
        private const val LONG_KEY = "longKey"
        private const val LONG_VALUE = 123456789L
        private const val DOUBLE_KEY = "doubleKey"
        private const val DOUBLE_VALUE = 3.14
        private const val PARCELABLE_KEY = "parcelableKey"
        private const val SERIALIZABLE_KEY = "serializableKey"
    }
}

The test testPutData is falling with the error:

Intent extras are null
java.lang.AssertionError: Intent extras are null

Why is this happening and how can I solve it?


Solution

  • Android's Intent class (as well as many other classes in the Android framework) has behavior that isn't just pure Java. These classes sometimes contain native or system-level code or depend on the Android runtime environment. This means they can't be tested with traditional JVM-based unit testing frameworks like JUnit in isolation. When you try to use such classes directly in a JUnit test, you'll often get unexpected errors or behavior.

    Robolectric is a library designed to let you test Android-specific code on the JVM. It provides shadow objects (shadows) for many of Android's classes. These shadows simulate the behavior of the real Android classes, making it possible to test Android code on your development machine without an emulator or device.

    The Intent extras are null error, it was likely because the vanilla JVM didn't know how to handle the Android-specific behavior of the Intent class. By using Robolectric:

    • Android Runtime Simulation: Robolectric sets up a simulated Android runtime environment, which allows the Intent and its methods to operate as they would on a real Android device or emulator.

    • Shadow Classes: Robolectric provides "shadow" implementations of many Android framework classes. When your tests call methods on these classes, they're actually calling methods on Robolectric's shadow objects, which mimic the behavior of the real classes.

    • Real Behavior without Emulators: Without Robolectric, to test this kind of behavior, you'd typically need to run the test on an Android emulator or a physical device, which would be slower and more cumbersome. Robolectric allows for quicker feedback and faster test execution.

    I resolved this problem using roboletric, putting this above the test class signature:

    @RunWith(RobolectricTestRunner::class)
    @Config(sdk = [Build.VERSION_CODES.R])
    

    The complete file is:

    import android.content.Intent
    import android.os.Build
    import android.os.Parcelable
    import com.mercadopago.selling.utils.extensions.addNewTaskFlags
    import com.mercadopago.selling.utils.extensions.putData
    import io.mockk.mockk
    import io.mockk.spyk
    import io.mockk.verify
    import java.io.Serializable
    import org.junit.Assert.assertEquals
    import org.junit.Test
    import org.junit.runner.RunWith
    import org.robolectric.RobolectricTestRunner
    import org.robolectric.annotation.Config
    
    @RunWith(RobolectricTestRunner::class)
    @Config(sdk = [Build.VERSION_CODES.R])
    class IntentExtensionTest {
    
        @Test
        fun testAddNewTaskFlags() {
            // When
            val mockIntent = spyk(Intent())
            mockIntent.addNewTaskFlags()
    
            // Then
            verify {
                mockIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_ANIMATION)
            }
        }
    
        @Test
        fun testPutData() {
            // Given
            val parcelableValue = mockk<Parcelable>()
            val serializableValue = mockk<Serializable>()
    
            // When
            val intent = Intent().putData(
                mapOf(
                    STRING_KEY to STRING_VALUE,
                    INT_KEY to INT_VALUE,
                    BOOLEAN_KEY to BOOLEAN_VALUE,
                    LONG_KEY to LONG_VALUE,
                    DOUBLE_KEY to DOUBLE_VALUE,
                    PARCELABLE_KEY to parcelableValue,
                    SERIALIZABLE_KEY to serializableValue
                )
            )
    
            // Then
            intent.run {
                val extras = extras ?: throw AssertionError("Intent extras are null")
    
                assertEquals(STRING_VALUE, extras.getString(STRING_KEY))
                assertEquals(INT_VALUE, extras.getInt(INT_KEY, -1))
                assertEquals(BOOLEAN_VALUE, extras.getBoolean(BOOLEAN_KEY, false))
                assertEquals(LONG_VALUE, extras.getLong(LONG_KEY, -1L))
                assertEquals(DOUBLE_VALUE, extras.getDouble(DOUBLE_KEY, -1.0), epsilon)
                assertEquals(parcelableValue, extras.getParcelable(PARCELABLE_KEY))
                assertEquals(serializableValue, extras.getSerializable(SERIALIZABLE_KEY))
            }
        }
    
        companion object {
            private const val STRING_KEY = "stringKey"
            private const val STRING_VALUE = "Hello, World!"
            private const val INT_KEY = "intKey"
            private const val INT_VALUE = 1
            private const val BOOLEAN_KEY = "booleanKey"
            private const val BOOLEAN_VALUE = true
            private const val LONG_KEY = "longKey"
            private const val LONG_VALUE = 123456789L
            private const val DOUBLE_KEY = "doubleKey"
            private const val DOUBLE_VALUE = 3.14
            private const val PARCELABLE_KEY = "parcelableKey"
            private const val SERIALIZABLE_KEY = "serializableKey"
            private const val epsilon = 1e-6
        }
    }