8

I'm working on an Android project that uses Type Safety Compose Navigation (androidx.navigation:navigation-compose:2.8.0-beta05). I’m trying to create a test to verify a value passed through SavedStateHandle.

See my code:

// Screen.kt

@Serializable
sealed class Screen {
    @Serializable
    data class Details(
        val movieId: Int,
    ) : Screen()
}
// DetailsViewModel.kt

import androidx.navigation.toRoute
import com.paulohc.movlist.navigation.Screen

class DetailsViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
) : ViewModel() {
    val movieId: Int = savedStateHandle.toRoute<Screen.Details>().movieId
}

I tried the following test:

// DetailsViewModelTest.kt

class DetailsViewModelTest {
    private lateinit var detailsViewModel: DetailsViewModel

    @BeforeEach
    fun setup() {
        val savedState = SavedStateHandle(mapOf("movieId" to 10))
        detailsViewModel = DetailsViewModel(savedState)
    }

    @Test
    fun getState() {
        assertThat(detailsViewModel.movieId).isEqualTo(10)
    }
}

But I am encountering the following error:

Method putInt in android.os.BaseBundle not mocked. See https://developer.android.com/r/studio-ui/build/not-mocked for details.
java.lang.RuntimeException: Method putInt in android.os.BaseBundle not mocked. See https://developer.android.com/r/studio-ui/build/not-mocked for details.

How can I rewrite this test to pass correctly? Can I mock the ´savedStateHandle´ with mockk?

2
  • 1
    Are you trying to test this with a JUnit test? Anything that references Android classes like Bundle (like these APIs do) needs to be an instrumented test or use Robolectric. Commented Aug 13, 2024 at 6:40
  • Yeah! I am trying test this with JUnit test. I noticed that savedStateHandle.toRoute() has an Android dependency on android.os.Bundle, as you can see in the following links: issuetracker.google.com/issues/349807172?pli=1 and issuetracker.google.com/issues/340966212?hl=it Commented Aug 13, 2024 at 18:07

2 Answers 2

5

Try using this Test rule bellow:

class SavedStateHandleRule(
    private val route: Any,
) : TestWatcher() {
    val savedStateHandleMock: SavedStateHandle = mockk()
    override fun starting(description: Description?) {
        mockkStatic("androidx.navigation.SavedStateHandleKt")
        every { savedStateHandleMock.internalToRoute<Any>(any(), any()) } returns route
        super.starting(description)
    }

    override fun finished(description: Description?) {
        unmockkStatic("androidx.navigation.SavedStateHandleKt")
        super.finished(description)
    }
}

The on the Test class:

@get:Rule
val savedStateHandleRule = SavedStateHandleRule(Screen.Details(movieId=123)

private val viewModel: DetailsViewModel get() = DetailsViewModel(savedStateHandleRule.savedStateHandleMock)
Sign up to request clarification or add additional context in comments.

1 Comment

Update: The workaround does not work with 2.9.0 anymore, since internalToRoute cannot be resolved.
4

I found workaround solution :

Using mockk

mockkStatic("androidx.navigation.SavedStateHandleKt")
every { saveStateHandle.toRoute<Page.Login>() } returns Page.Login(fromLogout = false)

also created an extension for it :

inline fun <reified T : Any> SavedStateHandle.mockkToRoute(page: T) {
    mockkStatic("androidx.navigation.SavedStateHandleKt")
    every { toRoute<T>() } returns page
}

Usage :

saveStateHandle.mockkToRoute(Page.Login(fromLogout = false))

The issue it using android dependencies android.os.Bundle so for not using robotelectric, i used it :)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.