Hilt テストガイド

Hilt のような依存関係の注入フレームワークを使用する利点の 1 つは、コードのテストが容易になる点です。

単体テスト

単体テストに 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-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 テストの設定

Hilt を使用する UI テストには @HiltAndroidTest アノテーションを付ける必要があります。このアノテーションは、各テストに 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.
}

4.2 より前のバージョンの Android Gradle プラグインを使用している場合は、モジュールの build.gradle ファイルで次の構成を適用して、ローカル単体テストの @AndroidEntryPoint クラスの変換を有効にします。

Groovy

hilt {
    enableTransformForLocalTests = true
}

Kotlin

hilt {
    enableTransformForLocalTests = true
}

enableTransformForLocalTests について詳しくは、Hilt のドキュメントをご覧ください。

機能のテスト

テストで Hilt を使用する準備ができたら、いくつかの機能を使用してテストのプロセスをカスタマイズできます。

テストに型を注入する

テストに型を注入するには、フィールド インジェクションのために @Inject を使用します。@Inject フィールドを挿入するように Hilt に指示するには、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 バインディングを置き換えるには、test フォルダまたは androidTest フォルダに新しい 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 モジュールに配置します。インストゥルメンテーション テストであれば、テスト バインディングを 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
}

マルチバインディングを追加する必要がある場合は、@BindValue の代わりに @BindValueIntoSet アノテーションと @BindValueIntoMap アノテーションを使用できます。@BindValueIntoMap の場合は、フィールドにマップキー アノテーションを付ける必要もあります。

特別な場合

Hilt では、標準的でないユースケースをサポートする機能も用意されています。

テスト用のカスタムアプリ

テストアプリで別のアプリを拡張する必要があるため HiltTestApplication を使用できない場合は、新しいクラスまたはインターフェースを @CustomTestApplication でアノテーションし、生成された Hilt アプリで拡張する基本クラスの値を渡します。

@CustomTestApplication は、Hilt でテストする Application クラスを生成します。このクラスは、パラメータとして渡したアプリを拡張するクラスです。

Kotlin

@CustomTestApplication(BaseApplication::class)
interface HiltTestApplication

Java

@CustomTestApplication(BaseApplication.class)
interface HiltTestApplication { }

この例では、BaseApplication クラスを拡張する HiltTestApplication_Application という Application が Hilt によって生成されます。一般に、生成されるアプリの名前は、アノテーションの付いたクラスの名前に _Application が追加されたものになります。生成された Hilt テストアプリを、インストゥルメンテーション テストまたは Robolectric テストで実行するように設定する必要があります(テストアプリをご覧ください)。

インストゥルメンテーション テスト内の複数の TestRule オブジェクト

テストに他の TestRule オブジェクトがある場合には、すべてのルールを共存させる方法が複数あります。

次のようにして、ルールを 1 つにまとめることができます。

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 は、@AndroidEntryPoint アノテーションが付いていないアクティビティに依存するため、Hilt では使用できません。

代わりに、GitHub リポジトリ(architecture-samples)のコード(launchFragmentInHiltContainer)を使用してください。

シングルトン コンポーネントが使用可能になる前にエントリ ポイントを使用する

Hilt テストのシングルトン コンポーネントが使用可能になる前に Hilt エントリ ポイントを作成する必要がある場合は、@EarlyEntryPoint アノテーションでエスケープ ハッチを使用できます。

@EarlyEntryPoint について詳しくは、Hilt のドキュメントをご覧ください。