-1

I'm trying to write a roboelectric compose test with code that looks like this:

@Composable
fun MyParentLayout() {
   val viewModel = hiltViewModel<MyParentViewModel>()
   MyParentImpl(viewModel.someValue)
}

@Composable
fun MyParentImpl(someValue:Int) {
  //A lot of displayed data
  MySubLayout()
  //A lot more data
}

@Composable
fun MySubLayout() {
   val viewModel = hiltViewModel<MySubViewModel>()
   //display code
}

I can easily refactor MySubLayout to remove the view model and test it directly like I did with the parent(and I have). But when trying to test MyParentImpl, it needs to call MySubLayout directly, which will make it get MySubViewModel, which will crash because in unit tests hilt isn't wired up.

I have a work around where we set a composition local value in the test, and have the code look for that override and use defaults instead of fetching the view model. But that seems a bit icky, and then we're not really testing the code we have in production. I could also pull up the call to get the view model 1 level and pass in the relevant state bits, but that becomes burdensome when it goes through multiple levels as it would in some cases. Is there a better way of dealing with this?

1
  • 2
    The usual way is indeed to hoist the view model out of the compoable to somewhere more suitable. If and how your real code can be (easily) refactored to accomodate this is something we cannot really answer here, though. Commented Oct 28 at 16:56

1 Answer 1

-1

The root cause for why its crashing:-

hiltViewModel() under the hood, is defined in androidx.hilt.navigation.compose.

@Composable
inline fun <reified VM : ViewModel, reified VMF> hiltViewModel(
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    },
    key: String? = null,
    noinline creationCallback: (VMF) -> VM
): VM { ... }

It does two key things:

  1. Finds the ViewModelStoreOwner for the current composition (usually the NavBackStackEntry or Activity).

  2. Gets a HiltViewModelFactory from the context:

    val factory = HiltViewModelFactory(LocalContext.current, owner)
    
    

That factory needs:

  • A Hilt Application (for dependency graph access)

  • A real SavedStateHandle (from an Android SavedStateRegistryOwner)

  • The @HiltViewModel annotation and generated components.

In Robolectric or plain Compose unit tests
  • There is no real HiltTestApplication or DI graph

  • There is no HiltViewModelFactory bound to your test environment

  • The composition’s LocalContext points to a Robolectric Application, not a HiltApplication

    Thus, calling causing the Crash.

There is many way to fix this issue:-


  1. Pass Dependencies via Parameters:- Basically instead of creating the viewmodel object in subview create the object at the Top.
@Composable
fun MyParentLayout(viewModel: MyParentViewModel = hiltViewModel()) {
    MyParentImpl(
        someValue = viewModel.someValue,
        subViewModel = hiltViewModel()
    )
}

@Composable
fun MyParentImpl(someValue: Int, subViewModel: MySubViewModel) {
    MySubLayout(subViewModel)
}

@Composable
fun MySubLayout(viewModel: MySubViewModel) {
    // display code
}

Then, in your test:

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun myParentImplDisplaysCorrectly() {
    val fakeSubViewModel = FakeSubViewModel()
    composeTestRule.setContent {
        MyParentImpl(someValue = 10, subViewModel = fakeSubViewModel)
    }
}

2. CompositionLocal Override for Testing

val LocalSubViewModel = staticCompositionLocalOf<MySubViewModel> {
    error("No SubViewModel provided")
}

@Composable
fun MySubLayout(
    viewModel: MySubViewModel = LocalSubViewModel.current
) {
   
}

In App:

CompositionLocalProvider(LocalSubViewModel provides hiltViewModel<MySubViewModel>()) {
    MySubLayout()
}

In tests:

val fakeViewModel = FakeSubViewModel()
composeTestRule.setContent {
    CompositionLocalProvider(LocalSubViewModel provides fakeViewModel) {
        MyParentImpl(someValue = 10)
    }
}

Recommended the 1st approach cause :-

  • Pure Compose test — no Hilt dependency.

  • Easy to inject fakes/mocks.

  • Works for both unit + integration testing.

2nd Approach:-

  • This is ideal when refactoring isn't feasible.
Sign up to request clarification or add additional context in comments.

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.