Hilt 测试指南

使用 Hilt 等依赖项注入框架的好处之一是它使测试代码更容易。

单元测试

单元测试不需要 Hilt,因为在测试使用构造函数注入的类时,不需要使用 Hilt 来实例化该类。相反,您可以通过传入伪造或模拟的依赖项直接调用类构造函数,就像构造函数没有注释一样。

Kotlin

@ActivityScoped
class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }

class AnalyticsAdapterTest {

  @Test
  fun `Happy path`() {
    // You don't need Hilt to create an instance of AnalyticsAdapter.
    // You can pass a fake or mock AnalyticsService.
    val adapter = AnalyticsAdapter(fakeAnalyticsService)
    assertEquals(...)
  }
}

Java

@ActivityScope
public class AnalyticsAdapter {

  private final AnalyticsService analyticsService;

  @Inject
  AnalyticsAdapter(AnalyticsService analyticsService) {
    this.analyticsService = analyticsService;
  }
}

public final class AnalyticsAdapterTest {

  @Test
  public void happyPath() {
    // You don't need Hilt to create an instance of AnalyticsAdapter.
    // You can pass a fake or mock AnalyticsService.
    AnalyticsAdapter adapter = new AnalyticsAdapter(fakeAnalyticsService);
    assertEquals(...);
  }
}

端到端测试

对于集成测试,Hilt 会像在生产代码中一样注入依赖项。使用 Hilt 进行测试无需维护,因为 Hilt 会为每个测试自动生成一组新的组件。

添加测试依赖项

要在测试中使用 Hilt,请在项目中包含 hilt-android-testing 依赖项。

Groovy

dependencies {
    // For Robolectric tests.
    testImplementation 'com.google.dagger:hilt-android-testing:2.51.1'
    // ...with Kotlin.
    kaptTest 'com.google.dagger:hilt-android-compiler:2.51.1'
    // ...with Java.
    testAnnotationProcessor 'com.google.dagger:hilt-android-compiler:2.51.1'


    // For instrumented tests.
    androidTestImplementation 'com.google.dagger:hilt-android-testing:2.51.1'
    // ...with Kotlin.
    kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.51.1'
    // ...with Java.
    androidTestAnnotationProcessor 'com.google.dagger:hilt-android-compiler:2.51.1'
}

Kotlin

dependencies {
    // For Robolectric tests.
    testImplementation("com.google.dagger:hilt-android-testing:2.51.1")
    // ...with Kotlin.
    kaptTest("com.google.dagger:hilt-android-compiler:2.51.1")
    // ...with Java.
    testAnnotationProcessor("com.google.dagger:hilt-android-compiler:2.51.1")


    // For instrumented tests.
    androidTestImplementation("com.google.dagger:hilt-android-testing:2.51.1")
    // ...with Kotlin.
    kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.51.1")
    // ...with Java.
    androidTestAnnotationProcessor("com.google.dagger:hilt-android-compiler:2.51.1")
}

UI 测试设置

必须使用 @HiltAndroidTest 注释任何使用 Hilt 的 UI 测试。此注释负责为每个测试生成 Hilt 组件。

此外,您需要将 HiltAndroidRule 添加到测试类中。它管理组件的状态,并用于在测试中执行注入。

Kotlin

@HiltAndroidTest
class SettingsActivityTest {

  @get:Rule
  var hiltRule = HiltAndroidRule(this)

  // UI tests here.
}

Java

@HiltAndroidTest
public final class SettingsActivityTest {

  @Rule
  public HiltAndroidRule hiltRule = new HiltAndroidRule(this);

  // UI tests here.
}

接下来,您的测试需要了解 Hilt 为您自动生成的 Application 类。

测试应用程序

必须在支持 Hilt 的 Application 对象中执行使用 Hilt 的工具测试。该库提供 HiltTestApplication 用于测试。如果您的测试需要不同的基本应用程序,请参阅 测试的自定义应用程序

您必须在 工具测试Robolectric 测试 中设置测试应用程序以运行。以下说明并非特定于 Hilt,而是关于如何在测试中指定要运行的自定义应用程序的一般指南。

在工具测试中设置测试应用程序

要在 工具测试 中使用 Hilt 测试应用程序,您需要配置一个新的测试运行器。这使得 Hilt 可以用于项目中的所有工具测试。执行以下步骤。

  1. androidTest 文件夹中创建一个扩展 AndroidJUnitRunner 的自定义类。
  2. 覆盖 newApplication 函数并传入生成的 Hilt 测试应用程序的名称。

Kotlin

// A custom runner to set up the instrumented application class for tests.
class CustomTestRunner : AndroidJUnitRunner() {

    override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
        return super.newApplication(cl, HiltTestApplication::class.java.name, context)
    }
}

Java

// A custom runner to set up the instrumented application class for tests.
public final class CustomTestRunner extends AndroidJUnitRunner {

  @Override
  public Application newApplication(ClassLoader cl, String className, Context context)
      throws ClassNotFoundException, IllegalAccessException, InstantiationException {
    return super.newApplication(cl, HiltTestApplication.class.getName(), context);
  }
}

接下来,如 工具单元测试指南 中所述,在您的 Gradle 文件中配置此测试运行器。确保使用完整的类路径。

Groovy

android {
    defaultConfig {
        // Replace com.example.android.dagger with your class path.
        testInstrumentationRunner "com.example.android.dagger.CustomTestRunner"
    }
}

Kotlin

android {
    defaultConfig {
        // Replace com.example.android.dagger with your class path.
        testInstrumentationRunner = "com.example.android.dagger.CustomTestRunner"
    }
}
在 Robolectric 测试中设置测试应用程序

如果您使用 Robolectric 测试 UI 层,则可以在 robolectric.properties 文件中指定要使用的应用程序。

application = dagger.hilt.android.testing.HiltTestApplication

或者,您可以使用 Robolectric 的 @Config 注释分别在每个测试中配置应用程序。

Kotlin

@HiltAndroidTest
@Config(application = HiltTestApplication::class)
class SettingsActivityTest {

  @get:Rule
  var hiltRule = HiltAndroidRule(this)

  // Robolectric tests here.
}

Java

@HiltAndroidTest
@Config(application = HiltTestApplication.class)
class SettingsActivityTest {

  @Rule public HiltAndroidRule hiltRule = new HiltAndroidRule(this);

  // Robolectric tests here.
}

如果您使用的 Android Gradle Plugin 版本低于 4.2,请通过在模块的 build.gradle 文件中应用以下配置来启用转换本地单元测试中的 @AndroidEntryPoint 类。

Groovy

hilt {
    enableTransformForLocalTests = true
}

Kotlin

hilt {
    enableTransformForLocalTests = true
}

有关 enableTransformForLocalTests 的更多信息,请参阅 Hilt 文档

测试功能

Hilt 准备好用于测试后,您可以使用多个功能来自定义测试过程。

在测试中注入类型

要在测试中注入类型,请对字段注入使用 @Inject。要告诉 Hilt 填充 @Inject 字段,请调用 hiltRule.inject()

以下是一个工具测试示例。

Kotlin

@HiltAndroidTest
class SettingsActivityTest {

  @get:Rule
  var hiltRule = HiltAndroidRule(this)

  @Inject
  lateinit var analyticsAdapter: AnalyticsAdapter

  @Before
  fun init() {
    hiltRule.inject()
  }

  @Test
  fun `happy path`() {
    // Can already use analyticsAdapter here.
  }
}

Java

@HiltAndroidTest
public final class SettingsActivityTest {

  @Rule public HiltAndroidRule hiltRule = new HiltAndroidRule(this);

  @Inject AnalyticsAdapter analyticsAdapter;

  @Before
  public void init() {
    hiltRule.inject();
  }

  @Test
  public void happyPath() {
    // Can already use analyticsAdapter here.
  }
}

替换绑定

如果您需要注入依赖项的伪造或模拟实例,则需要告诉 Hilt 不要使用它在生产代码中使用的绑定,而是使用不同的绑定。要替换绑定,您需要使用包含要在测试中使用的绑定的测试模块替换包含绑定的模块。

例如,假设您的生产代码声明 AnalyticsService 的绑定如下所示。

Kotlin

@Module
@InstallIn(SingletonComponent::class)
abstract class AnalyticsModule {

  @Singleton
  @Binds
  abstract fun bindAnalyticsService(
    analyticsServiceImpl: AnalyticsServiceImpl
  ): AnalyticsService
}

Java

@Module
@InstallIn(SingletonComponent.class)
public abstract class AnalyticsModule {

  @Singleton
  @Binds
  public abstract AnalyticsService bindAnalyticsService(
    AnalyticsServiceImpl analyticsServiceImpl
  );
}

要在测试中替换 AnalyticsService 绑定,请在 testandroidTest 文件夹中创建一个具有伪造依赖项的新 Hilt 模块,并使用 @TestInstallIn 对其进行注释。该文件夹中的所有测试都将注入伪造的依赖项。

Kotlin

@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [AnalyticsModule::class]
)
abstract class FakeAnalyticsModule {

  @Singleton
  @Binds
  abstract fun bindAnalyticsService(
    fakeAnalyticsService: FakeAnalyticsService
  ): AnalyticsService
}

Java

@Module
@TestInstallIn(
    components = SingletonComponent.class,
    replaces = AnalyticsModule.class
)
public abstract class FakeAnalyticsModule {

  @Singleton
  @Binds
  public abstract AnalyticsService bindAnalyticsService(
    FakeAnalyticsService fakeAnalyticsService
  );
}

在单个测试中替换绑定

要替换所有测试中的单个测试的绑定,请使用 @UninstallModules 注释从测试中卸载 Hilt 模块,并在测试中创建一个新的测试模块。

按照上一版本中的 AnalyticsService 示例,首先使用测试类中的 @UninstallModules 注释告诉 Hilt 忽略生产模块。

Kotlin

@UninstallModules(AnalyticsModule::class)
@HiltAndroidTest
class SettingsActivityTest { ... }

Java

@UninstallModules(AnalyticsModule.class)
@HiltAndroidTest
public final class SettingsActivityTest { ... }

接下来,您必须替换绑定。在定义测试绑定的测试类中创建一个新模块。

Kotlin

@UninstallModules(AnalyticsModule::class)
@HiltAndroidTest
class SettingsActivityTest {

  @Module
  @InstallIn(SingletonComponent::class)
  abstract class TestModule {

    @Singleton
    @Binds
    abstract fun bindAnalyticsService(
      fakeAnalyticsService: FakeAnalyticsService
    ): AnalyticsService
  }

  ...
}

Java

@UninstallModules(AnalyticsModule.class)
@HiltAndroidTest
public final class SettingsActivityTest {

  @Module
  @InstallIn(SingletonComponent.class)
  public abstract class TestModule {

    @Singleton
    @Binds
    public abstract AnalyticsService bindAnalyticsService(
      FakeAnalyticsService fakeAnalyticsService
    );
  }
  ...
}

这只会替换单个测试类的绑定。如果要替换所有测试类的绑定,请使用上面章节中的@TestInstallIn注解。或者,您可以将测试绑定放在 Robolectric 测试的test模块中,或者将测试绑定放在 instrumentation 测试的androidTest模块中。建议尽可能使用@TestInstallIn

绑定新值

使用@BindValue注解可以轻松地将测试中的字段绑定到 Hilt 依赖关系图中。使用@BindValue注解一个字段,它将在声明的字段类型下绑定,并包含该字段的任何限定符。

AnalyticsService示例中,您可以使用@BindValue替换AnalyticsService为模拟对象。

Kotlin

@UninstallModules(AnalyticsModule::class)
@HiltAndroidTest
class SettingsActivityTest {

  @BindValue @JvmField
  val analyticsService: AnalyticsService = FakeAnalyticsService()

  ...
}

Java

@UninstallModules(AnalyticsModule.class)
@HiltAndroidTest
class SettingsActivityTest {

  @BindValue AnalyticsService analyticsService = FakeAnalyticsService();

  ...
}

这简化了在测试中替换绑定和引用绑定的过程,因为它允许您同时执行这两项操作。

@BindValue支持限定符和其他测试注解。例如,如果您使用 Mockito 等测试库,则可以在 Robolectric 测试中按如下方式使用它:

Kotlin

...
class SettingsActivityTest {
  ...

  @BindValue @ExampleQualifier @Mock
  lateinit var qualifiedVariable: ExampleCustomType

  // Robolectric tests here
}

Java

...
class SettingsActivityTest {
  ...
  @BindValue @ExampleQualifier @Mock ExampleCustomType qualifiedVariable;

  // Robolectric tests here
}

如果您需要添加多绑定,可以使用@BindValueIntoSet@BindValueIntoMap注解代替@BindValue@BindValueIntoMap要求您还使用地图键注解注解该字段。

特殊情况

Hilt 还提供了一些功能来支持非标准用例。

自定义测试应用程序

如果您无法使用HiltTestApplication,因为您的测试应用程序需要扩展另一个应用程序,请使用@CustomTestApplication注解一个新类或接口,传入您希望生成的 Hilt 应用程序扩展的基类的值。

@CustomTestApplication将生成一个可用于 Hilt 测试的Application类,该类扩展了您作为参数传入的应用程序。

Kotlin

@CustomTestApplication(BaseApplication::class)
interface HiltTestApplication

Java

@CustomTestApplication(BaseApplication.class)
interface HiltTestApplication { }

在这个示例中,Hilt 生成了一个名为HiltTestApplication_ApplicationApplication,它扩展了BaseApplication类。通常,生成的应用程序的名称是带_Application后缀的注解类的名称。您必须设置生成的 Hilt 测试应用程序以便在您的instrumentation 测试Robolectric 测试中运行,如测试应用程序中所述。

instrumentation 测试中的多个 TestRule 对象

如果您的测试中还有其他TestRule对象,有多种方法可以确保所有规则都能一起工作。

您可以按如下方式将规则组合在一起:

Kotlin

@HiltAndroidTest
class SettingsActivityTest {

  @get:Rule
  var rule = RuleChain.outerRule(HiltAndroidRule(this)).
        around(SettingsActivityTestRule(...))

  // UI tests here.
}

Java

@HiltAndroidTest
public final class SettingsActivityTest {

  @Rule public RuleChain rule = RuleChain.outerRule(new HiltAndroidRule(this))
        .around(new SettingsActivityTestRule(...));

  // UI tests here.
}

或者,您可以将两个规则放在同一级别,只要HiltAndroidRule首先执行即可。使用@Rule注解中的order属性指定执行顺序。这只在 JUnit 4.13 或更高版本中有效。

Kotlin

@HiltAndroidTest
class SettingsActivityTest {

  @get:Rule(order = 0)
  var hiltRule = HiltAndroidRule(this)

  @get:Rule(order = 1)
  var settingsActivityTestRule = SettingsActivityTestRule(...)

  // UI tests here.
}

Java

@HiltAndroidTest
public final class SettingsActivityTest {

  @Rule(order = 0)
  public HiltAndroidRule hiltRule = new HiltAndroidRule(this);

  @Rule(order = 1)
  public SettingsActivityTestRule settingsActivityTestRule = new SettingsActivityTestRule(...);

  // UI tests here.
}

launchFragmentInContainer

无法从androidx.fragment:fragment-testing库中使用launchFragmentInContainer与 Hilt 一起使用,因为它依赖于未用@AndroidEntryPoint注解的 Activity。

请改用launchFragmentInHiltContainer 代码(来自architecture-samples GitHub 仓库)。

在单例组件可用之前使用入口点

当需要在 Hilt 测试中单例组件可用之前创建 Hilt 入口点时,@EarlyEntryPoint注解提供了一种应急方案。

有关@EarlyEntryPoint的更多信息,请参阅Hilt 文档