12

Dagger之十八、在测试中使用 Hilt

 2 years ago
source link: http://blog.chengyunfeng.com/?p=1130
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在测试中也可以使用 Hilt 来注入对象,配合使用 Hilt 测试将更加容易。通过 Hilt ,测试代码可以获取到 Dagger 所管理的依赖对象、还可以为测试提供专门的依赖对象。每个测试都有自己的 Hilt 部件,所以可以很容易的为每个测试提供不一样的依赖对象。

当前 Hilt 还处于 Alpha 预览版本,只支持 Android instrumentation 测试 和 Robolectric 测试,在 Android Studio 中运行测试还需要一点额外的配置。

app/build.gradle 中添加 Hilt 测试库

dependencies {
  implementation 'com.google.dagger:hilt-android:2.28-alpha'
  annotationProcessor 'com.google.dagger:hilt-android-compiler:2.28-alpha'
  // 用于 instrumentation 测试
  androidTestImplementation  'com.google.dagger:hilt-android-testing:2.28-alpha'
  androidTestAnnotationProcessor 'com.google.dagger:hilt-android-compiler:2.28-alpha'
  // 用于本地的单元测试
  testImplementation 'com.google.dagger:hilt-android-testing:2.28-alpha'
  testAnnotationProcessor 'com.google.dagger:hilt-android-compiler:2.28-alpha'

如果使用 Kotlin 的话,需要用 kapt 来替代 annotationProcessor:

dependencies {
  implementation 'com.google.dagger:hilt-android:2.28-alpha'
  kapt 'com.google.dagger:hilt-android-compiler:2.28-alpha'
  // 用于 instrumentation 测试
  androidTestImplementation  'com.google.dagger:hilt-android-testing:2.28-alpha'
  kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.28-alpha'
  // 用于本地的单元测试
  testImplementation 'com.google.dagger:hilt-android-testing:2.28-alpha'
  kaptTest 'com.google.dagger:hilt-android-compiler:2.28-alpha'
kapt {
// Hilt 需要设置 这个为 true,详情参考
// https://kotlinlang.org/docs/reference/kapt.html#non-existent-type-correction
correctErrorTypes true
// 单元测试配置,参考下面
hilt {
    enableTransformForLocalTests = true

默认情况下,Hilt gradle 插件会修改 instrumented 测试类的字节码(位于 androidTest 文件目录中的代码),但是对于本地单元测试代码需要一点额外的配置 Hilt 才能修改这些类(位于 test 文件目录中的代码)。

在项目 Gradle module 的 build.gradle 中,通过如下配置可以在单元测试中启用对 @AndroidEntryPoint 类的字节码修改:

hilt {
    enableTransformForLocalTests = true

只有当从命令行(通过 ./gradlew test 启动 Gradle)运行的时候,上面配置的 enableTransformForLocalTests 参数才生效。在 Android Studio 中通过测试类或者函数旁边的运行图标(绿色三角图标)来运行单元测试并不会生效。所以对于从 Android Studio 来直接运行测试,还需要如下额外的配置。

在 Android Studio 中运行测试

由于在 Android Studio 中运行测试的时候会忽略 Hilt 动态修改过的测试类。安卓开发团队正在研究如何修复这个问题,在该问题修复之前呢,有如下几种方式来避免这个问题。

第一种方法就是不要使用 Android Studio 来执行 Robolectric 测试,通过 Gradle 命令行来运行这些测试(比如 ./gradlew test 或者 ./gradlew testDebug)。

第二种方法是在 Android Studio 中创建一个自定义的测试配置,通过该配置让 Android Studio 通过 Gradle 来执行测试任务。

通过 Android Studio 菜单 Run -> Edit Configurations... 打开 Run/Debug Configuration 配置对话框,在该对话框中点击 + 号添加一个 Gradle 类型的配置,在该配置中名字可以随便设置,下面的配置中 Gradle project 为要执行测试的代码所在的 Gradle module; Task 为要执行的测试任务,通常为 test 或者 testDebug;Arguments 为需要执行的测试类,比如 --tests MyTestClassSee

https://dagger.dev/hilt/robolectric-test-configuration.jpg

在测试中使用 Hilt

经过上面的配置后,就可以开始在测试中使用 Hilt 了。要使用 Hilt 通常需要如下三个步骤:

  1. 在测试类上使用 @HiltAndroidTest
  2. 添加 HiltAndroidRule 作为测试的 Rule
  3. 使用 HiltTestApplication 作为测试的 Android Application
@HiltAndroidTest
public class FooTest {
  @Rule HiltAndroidRule rule = new HiltAndroidRule(this);

在 Robolectric 或者 instrumentation 测试中需要使用到 HiltTestApplication,在后面会详细介绍如何使用。

如果你的测试需要使用自定义的 Application 类,请参考后面的 自定义测试Application一小节的内容。

如果你的测试需要使用多个测试 Rules,请参考后面的 Hilt rule顺序一小节。

使用依赖对象

一个测试通常需要从 Hilt 部件中获取依赖对象。下面分别介绍如何从不同的安卓组件中来获取需要的依赖对象。

获取 ApplicationComponent 中的对象

ApplicationComponent 中的依赖对象可以通过 @Inject 注解来实现注入。注意:只有当调用了 HiltAndroidRule#inject() 函数以后,这些需要注入的变量才被赋值

@HiltAndroidTest
class FooTest {
  @Rule HiltAndroidRule hiltRule = new HiltAndroidRule(this);
  @Inject Foo foo;
  @Test
  public void testFoo() {
    assertThat(foo).isNull();
    hiltRule.inject(); // 启动对象注入
    assertThat(foo).isNotNull();

获取 ActivityComponent 中的对象

需要一个 Hilt Activity 实例才能从 ActivityComponent 中获取依赖对象。可以通过如下两种方式来实现这个操作。

第一种就是在测试类中定义个 Activity 内部类,并且在这个内部类中使用 @Inject 来定义需要注入的变量,然后通过这个内部类的对象来获取需要注入的依赖对象。

@HiltAndroidTest
class FooTest {
  @AndroidEntryPoint
  public static final class TestActivity extends AppCompatActivity {
    @Inject Foo foo;
  Foo foo = testActivity.foo;

当你的测试类中已经有 Hilt Activity 实例了,则可以通过定义一个 EntryPoint 来从 ActivityComponent 中获取里面的依赖对象。

@HiltAndroidTest
class FooTest {
  @EntryPoint
  @InstallIn(ActivityComponent.class)
  interface FooEntryPoint {
    Foo getFoo();
  Foo foo = EntryPoints.get(activity, FooEntryPoint.class).getFoo();

获取 FragmentComponent 中的对象

获取 FragmentComponent 中的依赖对象和上面的 Activity 操作方式类似。不同的地方在于,从 FragmentComponent部件中获取依赖对象不仅仅需要 Fragment 实例还需要该 Fragemnt 所在的 Activity 实例:

@HiltAndroidTest
class FooTest {
  @AndroidEntryPoint
  public static final class TestFragment extends Fragment {
    @Inject Foo foo;
  Foo foo = testFragment.foo;
// 或者通过 EntryPoint 来获取依赖对象
@HiltAndroidTest
class FooTest {
  @EntryPoint
  @InstallIn(FragmentComponent.class)
  interface FooEntryPoint {
    Foo getFoo();
  Foo foo = EntryPoints.get(fragment, FooEntryPoint.class).getFoo();

注意:由于当前没有办法来指定 Activity 类,所以 Hilt 还不支持 FragmentScenario,目前解决该问题的方式是启动一个 Hilt Activity,然后把 Fragment 添加到该 Activity 中。

添加测试依赖对象

在测试的时候,可能需要使用一些测试版本的依赖对象,这些对象只在测试中使用,在应用发布的时候并不包含这些对象;另外对于同一个类型的对象,可能测试需要的实例和应用发布中需要的实例也是有区别的。本节内容来介绍如何为测试环境提供不同的依赖对象。

内部 Dagger 模块

通常情况下,使用 @InstallIn 注解的模块在每个测试上都安装到了一个 Hilt 部件中。如果想把一些依赖对象只安装到部分测试上,可以在这个测试中使用内部 Dagger 模块

@HiltAndroidTest
public class FooTest {
  // 在 FooTest 中定义了一个内部的模块,该模块中的依赖对象只会存在于 FooTest 的部件中
  @Module
  @InstallIn(ApplicationComponent.class)
  static class FakeBarModule {
    @Provides
    static Bar provideBar(...) {
      // 对外提供 Bar 类型的对象,当需要该对象的时候,直接返回一个 FakeBar 对象
      return new FakeBar(...);

这样当两个不同的测试类都需要一个不同的 Bar 对象的时候,可以在每个测试类中定义自己的内部 Dagger 模块,他们两个互不干扰。

上面定义的是 static 类型的内部类模块,在静态模块中无法访问外部类的对象,如果你需要访问外部类中的对象,则可以删除 static 修饰符,把 模块 内部类定义为非静态的, 这样模块里面的 @Providers 函数可以获取外部类中的对象作为参数。

注意:在 Hilt 中 @InstallIn 模块的构造函数不能有参数。

@BindValue

对于上面这种简单的提供依赖对象的方式,可以通过 @BindValue 注解来更简单的实现上面的功能。

比如上面的 FakeBarModule 模块中的 @Providers 函数的实现很简单,只是通过 FakeBar 的构造函数来创建一个对象返回。那么使用 @BindValue 就可以更容易的实现该功能:

@HiltAndroidTest
public class FooTest {
  @BindValue Bar fakeBar = new FakeBar();

@BindValue 的意思是,当你需要注入一个 Bar 对象的时候, 直接用 fakeBar 这个变量的值即可。

由于 @BindValue 的变量定义到了测试类中并被该测试类控制,所以在 @BindValue 上是不支持范围修饰符的。 如果你想确保这个变量对象是单例的,则可以只在变量初始化的时候设置这个变量的值,其他地方不要修改这个变量的值即可。 如果该变量的状态是可变的,则可以在 @Before 函数中对该变量设置初始值,这样每个测试执行的时候该变量的值都是一样的。

同样对于 Dagger 中的绑定到集合功能 Hilt 也提供了支持。这些注解分别是 @BindValueIntoSet@BindElementsIntoSet@BindValueIntoMap

注意事项

如果使用了 ActivityScenarioRule,则需要特别注意 @BindValue非静态的内部模块类的使用。 ActivityScenarioRule 在调用 @Before 函数之前就创建好了 Activity 对象,所以当 @BindValue 变量在 @Before 函数里面初始化的时候,可能导致 Activity 中被注入的变量还没有被初始化。 所以对于这种情况,请在定义 @BindValue 变量的地方直接初始化该变量的值。

替换依赖对象

在测试的时候,把正式环境中的对象替换为测试环境中的对象是一个常见的需求,例如大量使用的 mock 对象。在 Hilt 测试中可以使用 @UninstallModules 注解来把正式环境中定义的模块给卸载了,然后再安装一个测试环境下的模块:

@UninstallModules(ProdFooModule.class)
@HiltAndroidTest
public class FooTest {
  @BindValue Foo fakeFoo = new FakeFoo();

比如上面的 ProdFooModule 模块是用于正式环境的,在该模块中提供了依赖对象 Foo 类,通过 @UninstallModules(ProdFooModule.class) 把该模块从测试环境中给卸载了,然后通过 @BindValue Foo fakeFoo = new FakeFoo(); 从新定义了依赖对象 Foo

如果在 ProdFooModule 模块中提供了两个对象 FooBar,但是在测试的时候你只需要替代 Foo,那么需要把 FooBar 分别定义到两个不同的模块中去,然后在测试环境中把包含 Foo 的模块给卸载了。

自定义测试Application

在测试的时候,每个测试都需要一个测试 Application, Hilt 定义了一个继承自 MultiDexApplication 的默认 HiltTestApplication。如果你需要使用自定义的 Application 的话,则需要使用 @CustomTestApplication 注解。

如果你的测试需要继承自一个特殊的 Application 类,则使用 @CustomTestApplication 可以生成一个继承自这个特殊类的 测试 Application。

在一个接口或者类中使用 @CustomTestApplication,并指定需要继承的特殊 Application 类即可。

// 会自动生成一个 MyCustom_Application.class
@CustomTestApplication(MyBaseApplication.class)
interface MyCustom {}

在上面的示例中, Hilt 生成一个名字为 MyCustom_Application 的类,该类会继承自 MyBaseApplication 这个类。

在测试中不推荐使用 @CustomTestApplication ,最好使用默认的 HiltTestApplication,这样将来更容易的组织和重用代码。如果你确实要使用自定义测试Application,那么有如下两点需要注意:

  1. 在 instrumentation 测试中,每个测试和每个测试中的测试函数都使用的是同一个 Application 实例,所以不要在自定义Application 类中保存应用的状态,避免不小心把一个测试中的状态给泄露到另外一个测试中去。
  2. 在测试Application 中,Hilt 部件并不是在 super#onCreate 函数中被创建的。这种原因是: @BindValue 这种特殊的功能需要访问测试对象实例引起的,只有当 Application#onCreate 函数创建后,这些测试对象实例才存在。所以和正式环境中不同,在测试环境中自定义的测试Application 不能在 Application#onCreate 函数中调用 Hilt 部件。 比如在正式环境中,在 Application#onCreate 中可以注入 Application 中需要注入的对象,但是在自定义测试Application中则不能这样操作。

Hilt rule 顺序

如果你的测试中使用了多个测试 Rule,确保 HiltAndroidRule 最先执行。比如 ActivityScenarioRule 调用 Activity#onCreate 函数,对于 Hilt Activity 需要在该函数中访问 Hilt 部件来执行注入操作。所以 ActivityScenarioRule 应该在 HiltAndroidRule 执行后再执行,这样确保 Activity 中的注入变量被正确赋值了。

@HiltAndroidTest
public class FooTest {
  // 把 HiltAndroidRule 放到 ActivityScenarioRule 的前面
  @Rule(order = 0) HiltAndroidRule hiltRule = new HiltAndroidRule(this);
  @Rule(order = 1)
  ActivityScenarioRule scenarioRule =
      new ActivityScenarioRule(MyActivity.class);

如果你使用的 JUnit 版本小于 4.13,则可以使用 RuleChain 来指定 Rule 的顺序。

Robolectric 测试

对于 Robolectric 测试所需要的 Application 可以通过 @Config 来配置也可以通过全局的 robolectric.properties来配置。 对于 Hilt 测试,该 Application 必须是 HiltTestApplication 或者是 自定义的测试 Application。

@HiltAndroidTest
@Config(application = HiltTestApplication.class)
public class FooTest {...}

当你的测试需要同时在 RobolectricAndroid instrumentation 环境中运行的时候,由于在 Android instrumentation 测试环境中无法访问 @Config 注解,所以只能使用 robolectric.properties 配置文件的方式来定义。

application=dagger.hilt.android.testing.HiltTestApplication

Instrumentation 测试

对于 Android instrumentation 测试,Application 对象可以通过自定义的 test runner 来提供。通过继承 AndroidJUnitRunner 类并重写 newApplication 函数:

package my.pkg;
public final class MyTestRunner extends AndroidJUnitRunner {
  @Override
  public Application newApplication(
      ClassLoader cl, String appName, Context context) {
    return super.newApplication(
        cl, HiltTestApplication.class.getName(), context);

然后把上面的自定义 Test runner 在 build.gradle 配置文件中配置一下:

android {
  defaultConfig {
      testInstrumentationRunner "my.pkg.MyTestRunner"

上面就是如何在测试环境中使用 Hilt 的介绍,最后一节课将通过一个示例项目一步一步的把项目迁移到 hilt 以及如何测试。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK