This article is the second article about Hilt MAD Skills series This time we focus on how to use Hilt to write tests and some best practices that need attention.
If you prefer a video about this content, can here view.
Hilt test concept
Since Hilt is a framework with specific processing principles, its test API is created based on some specific goals. Knowing the methods Hilt uses for testing helps you to use and understand its API. For more information about the test concept, please refer to: Hilt's test concept .
One of the core goals of the Hilt test API is to reduce the use of unnecessary false or simulated objects in the test, while using real objects as much as possible. Real objects can increase test coverage and are more resistant to future changes than fake or simulated objects. Fake or simulated objects are useful when real objects perform expensive tasks (such as IO operations). But they are often overused, and many people use it to solve problems that can be done conceptually in testing.
A related example is that if Dagger is used instead of Hilt, the test will be very troublesome. Setting up Dagger components for testing may require a lot of work and template code, but if you don't use Dagger and manually instantiate objects, it will lead to excessive use of mock objects. Let us see why this is the case.
Manual instantiation (Hilt is not used during testing)
Let's use an example to understand why manually instantiating objects in a test can lead to overuse of mock objects.
In the following code, we test the EventManager class with some dependencies. Since we don't want to configure the Dagger component for such a simple test, we directly instantiate the object manually.
class EventManager @Inject constructor(
dataModel: DataModel,
errorHandler: ErrorHandler
) {}
@RunWith(JUnit4::class)
class EventManagerTest {
@Test
fun testEventManager() {
val eventManager = EventManager(dataModel, errorHandler)
// 测试代码
}
}
In the beginning, since we just called the constructor like Dagger, everything seemed very simple. But when we need to solve the problem of how to obtain DataModel and ErrorHandler instances, the trouble comes:
@RunWith(JUnit4::class)
class EventManagerTest {
@Test
fun testEventManager() {
// 呃...changeNotifier 要怎么处理?
val dataModel = DataModel(changeNotifier)
val errorHandler = ErrorHandler(errorConfig)
val eventManager = EventManager(dataModel, errorHandler)
// 测试代码
}
}
We can also instantiate these objects directly, but if these objects also contain dependencies, it may be too deep to continue. Before the actual test, we may end up calling many constructors. In addition, these constructor calls will also make the test fragile. Any change in the constructor will break the test, even if they do not break anything in the production environment. Changes that should be "no operation", such as changing the order of parameters in the @Inject constructor, or adding dependencies to a class through the @Inject constructor, will break the test and make it difficult to update it.
To avoid this problem, people often just simulate the dependence on DataModel and ErrorHandler. But this is also a problem, because the introduction of these mock objects is not to avoid any expensive operations in the test, but only to deal with the test setup template code.
Use Hilt for testing
When Hilt is used, it will help you set up Dagger components so that you don't need to manually instantiate objects, and you can also avoid generating template code by configuring Dagger in your test. For more test content, please refer to complete test document .
To configure Hilt in your test, you need:
- @HiltAndroidTest annotations to your test
- Add test rule HiltAndroidRule
- HiltTestApplication for the Application class
For the third step, how to use HiltTestApplication depends on the type of your test:
- For Robolectric testing, please refer to the document .
- For instrumentation testing, please refer to the document .
After the configuration is complete, you can add the @Inject
field to your test to access the binding. These fields will be HiltAndroidRule
of inject()
, so you can do this in your setup method.
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class EventManagerTest {
@get:Rule
val rule = HiltAndroidRule(this)
@Inject
lateinit var eventManager: EventManager
@Before
fun setup() {
rule.inject(this)
}
@Test
fun testEventManager() {
// 使用注入的 eventManager 进行测试
}
}
It should be noted that the injected object must come from SingletonComponent . If you need ActivityComponent or FragmentComponent , you need to use the regular Android test API to create an Activity or Fragment and get dependencies from it.
Then you can start writing tests. The field you injected (in this case, our EventManager
class) will be constructed by Dagger for you just like in a production environment. You don't need to worry about any template code generated by managing dependencies.
TestInstallIn
When you encounter a situation where you need to replace dependencies in the test, for example, the real object will do expensive operations such as calling the server, you can use TestInstallIn to replace.
However, you cannot directly replace a binding in Hilt, but you can replace the module through TestInstallIn. The working form of TestInstallIn is similar to that of InstallIn, except that it also allows you to specify the modules that need to be replaced. The replaced module will not be used by Hilt, and any bindings added to the TestInstallIn module will be used. Similar to the InstallIn module, the TestInstallIn module is applied to all tests that depend on them (for example, all tests in the Gradle module).
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [BackendModule::class]
)
object FakeBackendModule {
@Singleton
@Provides
fun provideBackend(): BackendClient {
return FakeBackend.inMemoryBackendBuilder(
/* ...虚拟后台数据... */
).build()
}
}
UninstallModules
When you encounter a situation where you need to replace dependencies only in a single test, you can use UninstallModules . You can directly add the UninstallModules annotation to the test and use it to specify which modules Hilt should not use.
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
@UninstallModules(BackendModule::class)
class DataFetcherTest {
@BindValue
val fakeBackend = FakeBackend.inMemoryBackendBuilder(...).build()
...
}
In the test, you can use @BindValue or directly add bindings by defining nested components.
TestInstallIn vs UninstallModules
You may be wondering: Which of the two should be used? Let's make some comparisons between the two below:
TestInstallIn
- Apply to the whole world
- Easy to configure
- Conducive to increase the speed of construction
UninstallModules
- Only for a single test
- Very flexible
- Not conducive to build speed
Generally, we recommend starting from TestInstallIn
because it helps to increase the build speed. When you really need a separate configuration, you can still use UninstallModules
, but we recommend that you use it with caution only when you need it.
TestInstallIn/UninstallModules Reasons that affect the build speed
For each different module group used for testing, Hilt needs to create a new group of components. These components can end up being very large, especially when you rely on a lot of modules in production code.
△ Components generated for different module groups
UninstallModules
will add a new set of components that must be built, and the number of components may double based on the number of your tests . Since TestInstallIn
acts globally, it will add a default set of components, and this set can be shared among multiple tests. If you can change the test to make it unnecessary to use UninstallModules
, then you can reduce a set of components that need to be built.
But sometimes the test still needs to use UninstallModules
. It's ok! Just pay attention to the trade-offs and use TestInstallIn
default as much as possible.
test depends on
Another way to speed up test construction is to reduce the number of modules and entry points that are pulled into the test. This part will double UninstallModules
Sometimes, the actual coverage of your test is very small, but you may rely on all the production environment code. Since Hilt cannot be sure at compile time what you will test at runtime, Hilt must build a component that can find every module and entry point through your dependencies. These modules and entry points may be many, and may produce very large Dagger components, resulting in an increase in build time.
If you can reduce these dependencies, the new UninstallModules
may not generate much consumption, which allows you to be more flexible when configuring tests.
One way to reduce dependencies is to organize your Gradle modules. During this process, you can separate a large number of tests from the Gradle module of the main application to the Gradle module of the dependent library, thereby reducing the required dependencies.
△ Try to organize the tests into the Gradle module of the dependency library as much as possible
Organization Hilt module
Always remember to think about how to organize your Hilt, which will also help you write tests. We can often see very large Dagger modules with many bindings, but for Hilt, since you need to replace the entire module instead of a separate binding, those large modules that can do many things will only make the test more difficulty.
When using Hilt modules, you need to maintain their single purpose as much as possible. For this reason, you can even add only one public binding. This helps improve readability and makes it easier to replace them in the test when needed.
More resources
Applying the above practical content and understanding more of the trade-off ideas will help you to write Hilt tests more easily. For some of these APIs, which method you choose depends largely on how your application, testing, and build system are set up.
For more information about testing with Hilt, please refer to:
The above is all about the Hilt test. We are about to release more MAD Skills articles, so stay tuned.
Welcome to click here to submit feedback to us, or share your favorite content, found problems. Your feedback is very important to us, thank you for your support!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。