Hilt 测试最佳实践 | MAD Skills

语言: CN / TW / HK

本文是 MAD Skills 系列 中有关 Hilt 的第二篇文章。这次我们聚焦如何使用 Hilt 编写测试,以及一些需要注意的最佳实践。

如果您更喜欢通过视频了解此内容,可以 点击此处 查看.

Hilt 的测试理念

由于 Hilt 是一个有特定处理原则的框架,所以它的测试 API 是基于一些特定目标创建的。了解 Hilt 用于测试的方法有助于您使用和理解它的 API。如需进一步了解测试理念的更多信息,请参阅: Hilt 的测试理念

Hilt 测试 API 的一个核心目标,便是在测试中减少对不必要的虚假或模拟对象的使用,同时尽可能地使用真实对象。真实对象可以增加测试的覆盖率,并且相对于虚假或模拟的对象也更经得起日后的变化。当真实对象执行开销昂贵的任务 (例如 IO 操作) 时,虚假或模拟的对象便很有用。但它们经常被过度使用,很多人会用它来解决那些在概念上完全可以在测试中完成的问题。

一个相关例子是,如果使用了 Dagger 而没有用 Hilt, 测试时就会非常麻烦。为测试设置 Dagger 组件可能需要大量的工作和模板代码,但如果不用 Dagger 并手动实例化对象又会导致过度使用模拟对象。下面让我们看看为什么会这样。

手动实例化 (测试时不使用 Hilt)

让我们通过一个例子来了解为什么在测试中手动实例化对象会导致模拟对象的过度使用。

在下面的代码中,我们对含有一些依赖项的 EventManager 类进行测试。由于不想为这样简单的测试配置 Dagger 组件,所以我们直接手动实例化该对象。

```Kotlin class EventManager @Inject constructor( dataModel: DataModel, errorHandler: ErrorHandler ) {}

@RunWith(JUnit4::class) class EventManagerTest { @Test fun testEventManager() { val eventManager = EventManager(dataModel, errorHandler)

// 测试代码

} } ```

一开始,由于我们只是像 Dagger 一样调用了构造函数,所以一切看起来都十分简单。但当我们需要解决如何获得 DataModel与 ErrorHandler 实例的问题时,麻烦就来了:

```Kotlin @RunWith(JUnit4::class) class EventManagerTest { @Test fun testEventManager() { // 呃...changeNotifier 要怎么处理? val dataModel = DataModel(changeNotifier)
val errorHandler = ErrorHandler(errorConfig)

val eventManager = EventManager(dataModel, errorHandler)

// 测试代码

} } ```

我们也可以直接实例化这些对象,但是如果这些对象同样包含依赖,那么继续下去可能会过于深入。在进行实际测试前,我们最终可能会调用很多个构造函数。另外,这些构造函数的调用也会使测试变得脆弱。任何一个构造函数的改变都会破坏测试,即使它们在生产环境中没有破坏任何内容。本应为 "无操作" 的更改,例如在 @Inject 构造函数中改变参数顺序,或者通过 @Inject 构造函数为某个类添加依赖,都会破坏测试且难以对其进行更新。

为了避免这一问题,人们经常只是模拟对 DataModel 与 ErrorHandler 的依赖。但这同样是一个问题,因为引入这些模拟对象并不是为了避免测试中的任何昂贵操作,而只是为了处理测试的设置模板代码而已。

使用 Hilt 进行测试

使用 Hilt 时,它会帮您设置好 Dagger 组件,这样您便无需手动实例化对象,也能避免在测试中配置 Dagger 而产生模版代码。更多测试内容请参阅 完整的测试文档

若要在您的测试中配置 Hilt,您需要:

对于第三步来说,如何使用 HiltTestApplication 取决于您测试的类型:

  • 对于 Robolectric 测试,请查阅 文档
  • 对于插桩测试,请查阅 文档

配置完成后,您便可以为您的测试添加 @Inject 字段来访问绑定。这些字段会在您调用 HiltAndroidRuleinject() 后赋值,所以您可以在您的 setup 方法中完成这一操作。

```Kotlin @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 进行测试 } } ```

需要注意的是,注入的对象必须来自 SingletonComponent。如果您需要来自 ActivityComponentFragmentComponent 的对象,则需要使用常规 Android 测试 API 来创建一个 Activity 或 Fragment 并从中获取依赖。

随后您便可以开始编写测试了。您所注入的字段 (在本例中是我们的 EventManager 类) 将会像在生产环境中一样由 Dagger 为您构造。您无需担心管理依赖所产生的任何模版代码。

TestInstallIn

当您在测试中遇到需要替换依赖的情况,比如真实对象会做诸如调用服务器这样的昂贵操作时,您可以使用 TestInstallIn 来进行替换。

不过您无法直接在 Hilt 中替换某个绑定,但您可以通过 TestInstallIn 替换模块。TestInstallIn 的工作形式与 InstallIn 类似,不同之处在于它还允许您指定需要被替换的模块。被替换的模块将不会被 Hilt 使用,而任何加入 TestInstallIn 模块的绑定都会被使用。与 InstallIn 模块相似,TestInstallIn 模块会应用于所有依赖它们的测试 (例如 Gradle 模块中的所有测试)。

```Kotlin @Module @TestInstallIn( components = [SingletonComponent::class], replaces = [BackendModule::class] ) object FakeBackendModule {

@Singleton @Provides fun provideBackend(): BackendClient { return FakeBackend.inMemoryBackendBuilder( / ...虚拟后台数据... / ).build() } } ```

UninstallModules

当您遇到需要只在单个测试中替换依赖的情况时,可以使用 UninstallModules。您可以直接在测试上添加 UninstallModules 注解,并通过它指定 Hilt 不应使用哪些模块。

```Kotlin @HiltAndroidTest @RunWith(AndroidJUnit4::class) @UninstallModules(BackendModule::class) class DataFetcherTest {

@BindValue val fakeBackend = FakeBackend.inMemoryBackendBuilder(...).build()

... } ```

在测试中,您可以使用 @BindValue 或通过定义嵌套组件来直接添加绑定。

TestInstallIn vs UninstallModules

您也许会疑惑: 应该使用两者中的哪一个呢?下面我们对两者进行一些对比:

TestInstallIn

  • 应用于全局
  • 便于配置
  • 利于提升构建速度

UninstallModules

  • 只针对单个测试
  • 非常灵活
  • 不利于构建速度

通常,我们推荐从 TestInstallIn 开始,因为它有利于提升构建速度。当您确实需要单独的配置时,仍然可以使用 UninstallModules,但是我们建议您仅在特别需要时谨慎使用。

TestInstallIn/UninstallModules 影响构建速度的原因

对于每个用于测试的不同模块组,Hilt 需要创建一组新的组件。这些组件最终可能会非常大,当您依赖了大量生产代码中的模块时尤其如此。

△ 为不同模块组生成的组件

△ 为不同模块组生成的组件

UninstallModules 的每次使用都会添加一组必须被构建的新组件,组件的数量可能会基于您的测试数量而成倍增加 。而由于 TestInstallIn 作用于全局,所以它会加入一组组件的默认集合,而该集合可以在多个测试中共享。如果您可以通过改变测试而使其不必使用 UninstallModules,那么就可以减少一组需要构建的组件。

但有时测试还是需要使用 UninstallModules。没关系!只要注意权衡并尽可能默认使用 TestInstallIn 即可。

测试依赖

另一种可以加快测试构建速度的方式是减少拉入测试的模块和入口点。这个部分会在每次使用 UninstallModules 时翻倍。有时候,您测试的实际覆盖范围很小,却可能依赖了所有的生产环境代码。由于 Hilt 在编译时无法确定您将在运行时测试什么,因此 Hilt 必须构建一个可以通过您的依赖关系找到每个模块和入口点的组件。这些模块和入口点可能会很多,并且可能会产生很大的 Dagger 组件,从而导致构建时间的增加。

如果您可以减少这些依赖项,那么新增的 UninstallModules 可能不会产生太多消耗,从而可以让您在配置测试时更为灵活。

一种减少依赖的方法是组织您的 Gradle 模块,您可以在此过程中将大量测试从主应用的 Gradle 模块分离至依赖库的 Gradle 模块中,从而减少所需的依赖。

△ 尽可能将测试组织到依赖库 Gradle 模块中

△ 尽可能将测试组织到依赖库 Gradle 模块中

组织 Hilt 模块

要时刻记得考虑如何组织您的 Hilt,这也有助于您编写测试。我们常常能够看到十分巨大且拥有许多绑定的 Dagger 模块,但是对于 Hilt 来说,由于您需要替换整个模块而不是单独的绑定,那些可以做许多事的大型模块只会让测试变得更加困难。

在使用 Hilt 模块时,您需要尽可能地保持它们的单一目的性,为此甚至可以只加入一个公开的绑定。这有助于提高可读性,并在需要时可以更简单的在测试中替换它们。

更多资源

应用上述这些实践内容并了解更多其中权衡的思路,将会帮助您更轻松的编写 Hilt 测试。对于其中的一些 API 来说,您选择哪种方式很大程度上取决于您应用、测试以及构建系统的设置方式。

有关使用 Hilt 进行测试的更多信息,请查阅:

以上便是有关 Hilt 测试的全部内容,我们即将推出更多 MAD Skills 文章,敬请关注。

欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!