A JUnit rule for testing Android Fragments in projects based on Dagger's Hilt.
Hilt Fragment Rule is an additional weapon that decorates your Dagger's hilt.
This project aims to show a way for testing Android Fragments by directly manipulating
the LiveData
exposed by the companion ViewModel
. The core concept is to swap the implementation of
the Fragment's ViewModel during UI test. By doing so, you will be able to entirely control
UI's behavior during your test.
By default, Hilt provides the @ViewModelInject
annotation, which automagically binds your
ViewModel
instance into a Dagger Multibinding's Map
. This map is used by Hilt's
ViewModelProvider.Factory
implementation which is able to instantiate ViewModel
using the right
dependencies.
This is nice, but at time of writing there's no way to override that ViewModelProvider.Factory
.
Thus, injecting a test ViewModel
in our Fragment under test will not be possible.
That being said, here it comes the benefit of using Hilt Fragment Rule.
By following this approach, you should not use @ViewModelInject
but just rely on the (g)old
javax.inject.Inject
annotation.
Please have a look at this Fragment
and this ViewModel for a
reference implementation.
Disclaimer: This library is opinionated on a particular method of writing UI tests, so if your goal is to start adopting it in an existing project, you may need some refactor.
Ensure you have the Hilt Gradle plugin installed in your project. Open your project's build.gradle
and check you added it in the classpath. Also, add JitPack as Maven repository.
buildscript {
...
dependencies {
...
classpath "com.google.dagger:hilt-android-gradle-plugin:$hiltVersion"
}
}
allProjects {
repositories {
...
maven { url "https://jitpack.io" }
}
}
Then check your module's build.gradle
in which you should have both Kapt and Hilt's plugin enabled.
plugins {
...
id "kotlin-kapt"
id "dagger.hilt.android.plugin"
}
Down to the dependencies block, declare all test dependencies you may need, for example:
// Required for using Hilt in UI tests.
androidTestImplementation "com.google.dagger:hilt-android:$hiltVersion"
androidTestImplementation "com.google.dagger:hilt-android-testing:$hiltVersion"
kaptAndroidTest "com.google.dagger:hilt-compiler:$hiltVersion"
// JUnit and Espresso
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
// FragmentScenario utilities.
debugImplementation "androidx.fragment:fragment-testing:1.3.0-beta02"
Eventually, add hilt-fragment-rule
as dependency in your module's build.gradle
:
def version = "1.0.0"
androidTestImplementation "com.github.damianogiusti:hilt-fragment-rule:$version"
debugImplementation "com.github.damianogiusti:hilt-fragment-rule:$version"
Unlike the installation phases, setting up the test environment is pretty easy.
- Setup your test runner:
// app/build.gradle
android {
...
defaultConfig {
...
testInstrumentationRunner "com.damianogiusti.hilt.HiltFragmentTestRunner"
}
}
...
-
Into your
androidTest
source set, create a package namedandroidx.fragment.app.testing
and create a Java class namedFragmentScenario$EmptyFragmentActivity
. This will dynamically swap the originalandroidx.fragment.app.testing.FragmentScenario$EmptyFragmentActivity
which is defined insideandroidx.fragment:fragment-testing
library and which is the Activity used for launching Fragments usinglaunchFragmentInContainer
; -
Make
FragmentScenario$EmptyFragmentActivity
extendHiltFragmentBaseActivity
. This will let the magic happen.
package androidx.fragment.app.testing;
import com.damianogiusti.hilt.HiltFragmentBaseActivity;
public class FragmentScenario$EmptyFragmentActivity extends HiltFragmentBaseActivity {
}
- Create a class inside your
androidTest
that subclasses the Hilt's generated*.FragmentC
class. Have a look at sample project's implementation:
class TestFragmentComponent(fragment: Fragment) :
DefaultViewModelFactories.FragmentEntryPoint by HiltFragmentEntryPoint(fragment),
MainApp_HiltComponents.FragmentC() {
override fun injectHomeFragment(homeFragment: HomeFragment?) {
}
override fun viewWithFragmentComponentBuilder(): ViewWithFragmentComponentBuilder {
throw UnsupportedOperationException()
}
}
TestFragmentComponent
is the component that allows to override the ViewModel factory.
In particular, override is performed by delegating the implementation of DefaultViewModelFactories.FragmentEntryPoint
to HiltFragmentEntryPoint
. You can still write it manually if you want.
Be aware that each time you will add a Fragment annotated with
@AndroidEntryPoint
, this class will break because Hilt will generate another injector method. You will need to implement the injector method as displayed above. Let's say you add a fragment named UserFragment annotated with@AndroidEntryPoint
. In that case, you must implement theinjectUserFragment(userFragment: UserFragment?)
method. Without any doubt, this is a drawback, but (I believe) I can live with it.
A typical UI test will be like:
@HiltAndroidTest
class HomeFragmentTest {
@JvmField @Rule val hiltFragmentRule = HiltFragmentRule<TestFragmentComponent>()
@JvmField @Rule val hiltTestRule = HiltAndroidRule(this)
@Test
fun fragment_shows_text_emitted_by_ViewModel() {
launchFragmentInContainer<HomeFragment>(TestHomeViewModel.Factory()) {
onViewModel<TestHomeViewModel> { viewModel ->
viewModel.message.value = "Hello from tests!"
}
onView(withId(R.id.home_text_view)).check(matches(withText("Hello from tests!")))
onViewModel<TestHomeViewModel> { viewModel ->
viewModel.message.value = "Again, cheers!"
}
onView(withId(R.id.home_text_view)).check(matches(withText("Again, cheers!")))
}
}
}
private class TestHomeViewModel : HomeViewModel() {
override val message = MutableLiveData<String>()
class Factory : ViewModelProvider.Factory {
private val viewModel = TestHomeViewModel()
override fun <T : ViewModel?> create(modelClass: Class<T>) = viewModel as T
}
}
-
HiltFragmentRule
is the JUnit rule that -
launchFragmentInContainer
is an utility function that I suggest you to add to you project. You can find it here; -
onViewModel
provides an instance of the ViewModel associated with the Fragment under test; -
TestHomeViewModel
is the test implementation of ourHomeViewModel
; -
TestHomeViewModel.Factory
is aViewModelProvider.Factory
implementation that provides a single instance ofTestHomeViewModel
. I suggest you to store as instance variable the ViewModel that your factory will provide. Why? If you take a look at the sample app,HomeFragment
is asking for aHomeViewModel
instance and our test class is asking for aTestHomeViewModel
instance. Even if ourTestHomeViewModel.Factory
will always return instances ofTestHomeViewModel
, the test class will end up interacting with a different ViewModel instance, because the Fragment's ViewModel will be associated to aClass<TestHomeViewModel>
key inside theViewModelStore
. Thus any action you will perform will have no effect.
Damiano Giusti – damianogiusti.com
Copyright 2020 Damiano Giusti
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.