Guida al test dell'hilt

Uno dei vantaggi dell'utilizzo di framework di dipendenza come Hilt è che semplifica il test del codice.

Test delle unità

Hilt non è necessario per i test di unità, poiché quando testi una classe che utilizza l'iniezione del costruttore, non devi utilizzare Hilt per creare un'istanza della classe. In alternativa, puoi chiamare direttamente un costruttore di classe passando dipendenze false o simulate, come faresti se il costruttore non fosse annotato:

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(...);
  }
}

Test end-to-end

Per i test di integrazione, Hilt inietta le dipendenze come farebbe nel codice di produzione. I test con Hilt non richiedono manutenzione perché Hilt genera automaticamente un nuovo insieme di componenti per ogni test.

Aggiunta di dipendenze di test

Per utilizzare Hilt nei test, includi la dipendenza hilt-android-testing nel progetto:

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")
}

Configurazione del test dell'interfaccia utente

Devi annotare qualsiasi test dell'interfaccia utente che utilizza Hilt con @HiltAndroidTest. Questa annotazione è responsabile della generazione dei componenti Hilt per ogni test.

Inoltre, devi aggiungere HiltAndroidRule al test class. Gestisce lo stato dei componenti e viene utilizzato per eseguire l'iniezione nel test:

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.
}

Successivamente, il test deve conoscere la classe Application che Hilt genera automaticamente per te.

Testare l'applicazione

Devi eseguire test instrumentati che utilizzano Hilt in un oggetto Application che supporta Hilt. La libreria fornisce HiltTestApplication da utilizzare nei test. Se i test richiedono un'applicazione di base diversa, consulta Applicazione personalizzata per i test.

Devi impostare l'applicazione di test in modo che venga eseguita nei test instrumentati o nei test Robolectric. Le istruzioni riportate di seguito non sono specifiche per Hilt, ma sono linee guida generali su come specificare un'applicazione personalizzata da eseguire nei test.

Impostare l'applicazione di test nei test con strumenti

Per utilizzare l'applicazione di test Hilt nei test instrumentati, devi configurare un nuovo programma di test. In questo modo, Hilt funziona per tutti i test instrumentati nel progetto. Procedi come indicato di seguito:

  1. Crea una classe personalizzata che estenda AndroidJUnitRunner nella cartella androidTest.
  2. Sostituisci la funzione newApplication e passa il nome dell'applicazione di test Hilt generata.

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);
  }
}

Successivamente, configura questo runner di test nel file Gradle come descritto nella guida ai test di unità instrumentati. Assicurati di utilizzare il percorso di classe completo:

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"
    }
}
Impostare l'applicazione di test nei test Robolectric

Se utilizzi Robolectric per testare il livello UI, puoi specificare l'applicazione da utilizzare nel file robolectric.properties:

application = dagger.hilt.android.testing.HiltTestApplication

In alternativa, puoi configurare l'applicazione su ogni test singolarmente utilizzando l'annotazione @Config di Robolectric:

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.
}

Se utilizzi una versione del plug-in Android per Gradle precedente alla 4.2, attiva la trasformazione delle classi @AndroidEntryPoint nei test di unità locali applicando la seguente configurazione nel file build.gradle del modulo:

Groovy

hilt {
    enableTransformForLocalTests = true
}

Kotlin

hilt {
    enableTransformForLocalTests = true
}

Scopri di più su enableTransformForLocalTests nella documentazione di Hilt.

Funzionalità di test

Una volta che Hilt è pronto per essere utilizzato nei test, puoi utilizzare diverse funzionalità per personalizzare la procedura di test.

Esegui l'iniezione di tipi nei test

Per iniettare tipi in un test, utilizza @Inject per l'iniezione di campi. Per indicare a Hilt di compilare i campi @Inject, chiama hiltRule.inject().

Vedi il seguente esempio di test strumentato:

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.
  }
}

Sostituire un'associazione

Se devi iniettare un'istanza falsa o simulata di una dipendenza, devi dire a Hilt di non utilizzare il binding utilizzato nel codice di produzione e di utilizzarne uno diverso. Per sostituire una associazione, devi sostituire il modulo che la contiene con un modulo di test che contiene le associazioni che vuoi utilizzare nel test.

Ad esempio, supponiamo che il codice di produzione dichiari una associazione per AnalyticsService come segue:

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
  );
}

Per sostituire il binding AnalyticsService nei test, crea un nuovo modulo Hilt nella cartella test o androidTest con la dipendenza falsa e annotalo con @TestInstallIn. A tutti i test in quella cartella viene invece iniettata la dipendenza falsa.

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
  );
}

Sostituire una associazione in un singolo test

Per sostituire una associazione in un singolo test anziché in tutti i test, disinstalla un modulo Hilt da un test utilizzando l'annotazione @UninstallModules e crea un nuovo modulo di test all'interno del test.

Seguendo l'esempio AnalyticsService della versione precedente, inizia chiedendo a Hilt di ignorare il modulo di produzione utilizzando l'annotazione @UninstallModules nella classe di test:

Kotlin

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

Java

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

Successivamente, devi sostituire il binding. Crea un nuovo modulo all'interno della classe di test che definisce il binding del test:

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
    );
  }
  ...
}

Viene sostituita solo la definizione per una singola classe di test. Se vuoi sostituire l'associazione per tutti i classi di test, utilizza l'annotazione @TestInstallIn della sezione precedente. In alternativa, puoi inserire il binding del test nel modulo test per i test Robolectric o nel modulo androidTest per i test con strumenti. Ti consigliamo di utilizzare @TestInstallIn, se possibile.

Associare nuovi valori

Utilizza l'annotazione @BindValue per associare facilmente i campi del test al grafico di dipendenza di Hilt. Se annoti un campo con @BindValue, questo verrà vincolato al tipo di campo dichiarato con eventuali qualificatori presenti per quel campo.

Nell'esempio AnalyticsService, puoi sostituire AnalyticsService con un valore falso utilizzando @BindValue:

Kotlin

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

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

  ...
}

Java

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

  @BindValue AnalyticsService analyticsService = FakeAnalyticsService();

  ...
}

In questo modo, puoi semplificare sia la sostituzione di una associazione sia il riferimento a una associazione nel test, poiché puoi eseguire entrambe le operazioni contemporaneamente.

@BindValue funziona con i qualificatori e altre annotazioni di test. Ad esempio, se utilizzi librerie di test come Mockito, puoi utilizzarla in un test Robolectric come segue:

Kotlin

...
class SettingsActivityTest {
  ...

  @BindValue @ExampleQualifier @Mock
  lateinit var qualifiedVariable: ExampleCustomType

  // Robolectric tests here
}

Java

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

  // Robolectric tests here
}

Se devi aggiungere una associazione multipla, puoi utilizzare le annotazioni @BindValueIntoSet e @BindValueIntoMap al posto di @BindValue. @BindValueIntoMap richiede di annotare anche il campo con un'annotazione della legenda della mappa.

Casi particolari

Hilt fornisce anche funzionalità per supportare casi d'uso non standard.

Applicazione personalizzata per i test

Se non puoi utilizzare HiltTestApplication perché l'applicazione di test deve estendere un'altra applicazione, annota una nuova classe o interfaccia con @CustomTestApplication, passando il valore della classe di base che vuoi che l'applicazione Hilt generata estenda.

@CustomTestApplication genererà una classe Application pronta per il test con Hilt che estende l'applicazione passata come parametro.

Kotlin

@CustomTestApplication(BaseApplication::class)
interface HiltTestApplication

Java

@CustomTestApplication(BaseApplication.class)
interface HiltTestApplication { }

Nell'esempio, Hilt genera un Application chiamato HiltTestApplication_Application che estende la classe BaseApplication. In generale, il nome dell'applicazione generata è il nome della classe annotata con _Application. Devi impostare l'applicazione di test Hilt generata in modo che venga eseguita nei test con strumenti o nei test Robolectric come descritto in App di test.

Più oggetti TestRule nel test instrumentato

Se nel test sono presenti altri oggetti TestRule, esistono diversi modi per assicurarti che tutte le regole funzionino insieme.

Puoi raggruppare le regole come segue:

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.
}

In alternativa, puoi utilizzare entrambe le regole allo stesso livello, a condizione che la regola HiltAndroidRule venga eseguita per prima. Specifica l'ordine di esecuzione utilizzando l'attributo order nell'annotazione @Rule. Questo funziona solo nella versione JUnit 4.13 o successive:

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

Non è possibile utilizzare launchFragmentInContainer dalla biblioteca androidx.fragment:fragment-testing con Hilt, perché si basa su un'attività non annotata con @AndroidEntryPoint.

Utilizza invece il codice launchFragmentInHiltContainer del repository GitHub architecture-samples.

Utilizza un punto di contatto prima che il componente singleton sia disponibile

L'annotazione @EarlyEntryPoint fornisce una via di fuga quando è necessario creare un punto di accesso Hilt prima che il componente singleton sia disponibile in un test Hilt.

Scopri di più su @EarlyEntryPoint nella documentazione di Hilt.