google / dagger

A fast dependency injector for Android and Java.
https://dagger.dev
Apache License 2.0
17.42k stars 2.01k forks source link

Allow testing fragments without @HiltAndroidTest #2543

Closed magnusvs closed 3 years ago

magnusvs commented 3 years ago

Hi,

I'm trying to migrate from a setup with dagger.android to use the new Hilt functionality and I've run into problems with some tests.

I have since earlier tests that run on some "isolated" Fragments that uses a mocked ViewModel.

To accomplish switching out the ViewModel in the tests a custom ViewModelProvider.Factory is passed in the constructor to the Fragment. And used like:

val viewModel by viewModels<SomeViewModel> { vmFactory }

I then create a robolectric test:

@RunWith(RobolectricTestRunner::class)
class SomeFragmentTest {

    private val viewModel = ...

    @Test
    fun `Testing fragment is working`() {
        // setup ViewModel state etc

        launchFragment().onFragment {

            // Asserting things
        }
    }

    private fun launchFragment(): FragmentScenario<SomeFragment> {
        return launchFragmentInContainer(
            factory = FakeFragmentFactory(viewModel) {
                SomeFragment(it)
            },
        )
    }
}

class FakeFragmentFactory<T : Fragment>(
    private val viewModel: ViewModel,
    private val creator: (ViewModelProvider.Factory) -> T
) : FragmentFactory() {

    override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
        val fakeViewModelFactory = FakeViewModelFactory(viewModel)
        return creator(fakeViewModelFactory)
    }
}

@Suppress("UNCHECKED_CAST")
class FakeViewModelFactory(private val testViewModel: ViewModel) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>) = testViewModel as T
}

This kind of test runs perfectly fine in Android Studio but then fail when run through a terminal ./gradlew testDebugUnitTest Failing with

Hilt classes generated from @HiltAndroidTest are missing. Check that you have annotated your test class with @HiltAndroidTest and that the processor is running over your test

  1. Which behaviour is correct? Should the test fail or pass?
  2. Can dagger/hilt support launching a fragment without any injection etc. Or is it always considered bad practice to do these kinds of "unit tests" on a Fragment?
Chang-Eric commented 3 years ago

Hm, something seems weird here. In general, you should always need @HiltAndroidTest if you want Hilt to generate components, etc for you. However, in this case, it isn't clear to me if you want that. I think this error may occur if you try to use the HiltTestApplication with a test that isn't @HiltAndroidTest. I'm not sure why this would differ with Android Studio vs Gradle on the command line.

If your fragment is an @AndroidEntryPoint fragment, however, then in general, you do need to run with injection. Otherwise your @Inject fields won't be set. Fragments on attach will try to look for the Dagger component to set these fields and fail if they aren't attached to a Hilt application/activity. If you really wanted to, you could use https://dagger.dev/hilt/optional-inject which will remove this restriction and let you run the fragment without Dagger/Hilt. But that isn't really something I would recommend as a testing strategy and testing isn't the intended use of optional injection. It might work out as a short-term migration strategy though.

magnusvs commented 3 years ago

Yea I can understand this use case is probably a bit weird. No I really don't need Hilt at all in these tests, a bit similar to how a ViewModel doesn't really need hilt for testing. Can just pass dependencies manually for unit tests. I don't think this comes from HiltTestApplication though since I haven't imported any hilt testing libraries.

Since I had another setup for tests I was looking for a way to avoid the checks that the fragments lives in a hilt application & activity. Found it a bit weird that the tests passes in Android Studio since I was using the normal FragmentScenario, no activity with hilt started the fragment. Annotating the Fragments with @OptionalInject makes the tests pass on the command line as well. So I could go with that for now since I don't have a lot of these tests and then improve the testing strategy later.

Is it worth looking into why there's a difference between Android Studio and Gradle from command line? Or should we just ignore what seems to be a weird use case and close this?

Chang-Eric commented 3 years ago

I don't think this comes from HiltTestApplication though since I haven't imported any hilt testing libraries.

That's very strange since as far as I can tell that's the only place that particular error message could come from. Anyway, I think it'd be fine to close this out for now and revisit if issues pop up again. FWIW, while it is possible to test these things by just passing in dependencies manually, Hilt is designed to avoid this type of testing which you can read more about here: https://dagger.dev/hilt/testing-philosophy.