Przewodnik po testowaniu śladu

Jedną z zalet korzystania z platform wstrzykiwania zależności, takich jak Hilt, jest to, że ułatwiają one testowanie kodu.

Testy jednostkowe

Hilt nie jest potrzebny do testów jednostkowych, ponieważ podczas testowania klasy, która korzysta z wstrzykiwania przez konstruktor, nie musisz używać Hilt do utworzenia instancji tej klasy. Zamiast tego możesz bezpośrednio wywołać konstruktor klasy, przekazując fałszywe lub pozorowane zależności, tak jak w przypadku, gdyby konstruktor nie był oznaczony adnotacją:

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

To samo dotyczy klas ViewModel uzyskanych przez wywołanie hiltViewModel() w elementach kompozycyjnych. W testach jednostkowych utwórz ViewModel bezpośrednio za pomocą fałszywych danych. Więcej informacji o tym, jak stan przepływa z ViewModel do elementów kompozycyjnych, znajdziesz w artykułach Stan i Jetpack Compose oraz Gdzie podnieść stan.

Testy kompleksowe

W testach integracji Hilt wstrzykuje zależności tak samo jak w kodzie produkcyjnym. Testowanie za pomocą Hilt nie wymaga konserwacji, ponieważ Hilt automatycznie generuje nowy zestaw komponentów dla każdego testu.

Dodawanie zależności testowych

Aby używać Hilt w testach, dodaj do projektu zależność hilt-android-testing:

dependencies {
    // For Robolectric tests.
    testImplementation("com.google.dagger:hilt-android-testing:2.57.1")
    kspTest("com.google.dagger:hilt-android-compiler:2.57.1")

    // For instrumented tests.
    androidTestImplementation("com.google.dagger:hilt-android-testing:2.57.1")
    kspAndroidTest("com.google.dagger:hilt-android-compiler:2.57.1")

    // Compose UI test rule.
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-test-manifest")

}

Konfiguracja testu interfejsu

Każdy test interfejsu, który korzysta z Hilt, musi być oznaczony adnotacją @HiltAndroidTest. Ta adnotacja odpowiada za generowanie komponentów Hilt dla każdego testu.

Musisz też dodać HiltAndroidRule do klasy testowej. Zarządza ona stanem komponentów i służy do wstrzykiwania w teście:

@HiltAndroidTest
class SettingsScreenTest {

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

    @get:Rule(order = 1)
    val composeRule = createAndroidComposeRule<HiltTestActivity>()

    // Compose UI tests here.
}

Następnie test musi znać klasę Application, którą Hilt automatycznie generuje.

Aby umożliwić Hilt wstrzykiwanie zależności, musisz utworzyć pustą aktywność o nazwie HiltTestActivity w zbiorze źródeł androidTest i oznaczyć ją adnotacją @AndroidEntryPoint. createAndroidComposeRule używa tej aktywności jako hosta dla treści kompozycyjnych.

Testuj aplikację

Testy instrumentowane, które korzystają z Hilt, musisz wykonywać w obiekcie Application, który obsługuje Hilt. Biblioteka udostępnia HiltTestApplication do użycia w testach. Jeśli testy wymagają innej aplikacji podstawowej, przeczytaj artykuł Aplikacja niestandardowa do testów.

Musisz ustawić aplikację testową tak, aby była uruchamiana w testach instrumentowanych lub testach Robolectric. Poniższe instrukcje nie dotyczą konkretnie Hilt, ale zawierają ogólne wskazówki, jak określić aplikację niestandardową do uruchamiania w testach.

Ustawianie aplikacji testowej w testach instrumentowanych

Aby używać aplikacji testowej Hilt w instrumentowanych testach, musisz skonfigurować nowy program do uruchamiania testów. Dzięki temu Hilt będzie działać we wszystkich testach instrumentowanych w projekcie. Wykonaj te czynności:

  1. Utwórz klasę niestandardową, która rozszerza AndroidJUnitRunner w folderze androidTest.
  2. Zastąp funkcję newApplication i przekaż nazwę wygenerowanej aplikacji testowej Hilt.
// 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)
    }
}

Następnie skonfiguruj ten program do uruchamiania testów w pliku Gradle zgodnie z opisem w przewodniku po testach jednostkowych instrumentowanych. Upewnij się, że używasz pełnej ścieżki klasy:

android {
    defaultConfig {
        // Replace com.example.android.dagger with your class path.
        testInstrumentationRunner = "com.example.android.dagger.CustomTestRunner"
    }
}
Ustawianie aplikacji testowej w testach Robolectric

Jeśli używasz Robolectric do testowania warstwy interfejsu, możesz określić, której aplikacji używać, w pliku robolectric.properties:

application = dagger.hilt.android.testing.HiltTestApplication

Możesz też skonfigurować aplikację w każdym teście osobno, używając adnotacji @Config Robolectric:

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

  @get:Rule
  var hiltRule = HiltAndroidRule(this)

  // Robolectric tests here.
}

Funkcje testowania

Gdy Hilt będzie gotowy do użycia w testach, możesz użyć kilku funkcji, aby dostosować proces testowania.

Wstrzykiwanie typów w testach

Aby wstrzyknąć typy do testu, użyj @Inject do wstrzykiwania przez pola. Aby poinformować Hilt, że ma wypełnić pola @Inject, wywołaj hiltRule.inject().

Oto przykład testu instrumentowanego:

@HiltAndroidTest
class SettingsScreenTest {

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

    @get:Rule(order = 1)
    val composeRule = createAndroidComposeRule<HiltTestActivity>()

    @Inject
    lateinit var analyticsAdapter: AnalyticsAdapter

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

    @Test
    fun settingsScreen_showsTitle() {
        composeRule.setContent {
            SettingsScreen()
        }
        composeRule.onNodeWithText("Settings").assertIsDisplayed()
        // analyticsRepository is available here.
    }
}

Zastępowanie powiązania

Jeśli musisz wstrzyknąć fałszywą lub pozorowaną instancję zależności, musisz poinformować Hilt, aby nie używał powiązania, którego używał w kodzie produkcyjnym, i zamiast tego używał innego. Aby zastąpić powiązanie, musisz zastąpić moduł, który zawiera powiązanie, modułem testowym, który zawiera powiązania, których chcesz użyć w teście.

Załóżmy na przykład, że kod produkcyjny deklaruje powiązanie z AnalyticsService w ten sposób:

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

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

Aby zastąpić powiązanie AnalyticsService w testach, utwórz nowy moduł Hilt w folderze test lub androidTest z fałszywą zależnością i oznacz go adnotacją @TestInstallIn. We wszystkich testach w tym folderze zostanie wstrzyknięta fałszywa zależność.

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

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

Ponieważ elementy kompozycyjne zwykle korzystają z tych zależności pośrednio za pomocą ViewModel uzyskanej za pomocą hiltViewModel(), wystarczy zastąpić powiązanie w Hilt. Testowany element kompozycyjny automatycznie pobiera fałszywe dane.

Zastępowanie powiązania w jednym teście

Aby zastąpić powiązanie w jednym teście zamiast we wszystkich testach, odinstaluj moduł Hilt z testu za pomocą adnotacji @UninstallModules i utwórz nowy moduł testowy w teście.

Kontynuując przykład AnalyticsService z poprzedniej wersji, zacznij od poinformowania Hilt, aby ignorował moduł produkcyjny, używając adnotacji @UninstallModules w klasie testowej:

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

Następnie musisz zastąpić powiązanie. Utwórz nowy moduł w klasie testowej, który definiuje powiązanie testowe:

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

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

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

  // ...
}

Zastąpi to powiązanie tylko w przypadku jednej klasy testowej. Jeśli chcesz zastąpić powiązanie we wszystkich klasach testowych, użyj adnotacji @TestInstallIn z sekcji powyżej. Możesz też umieścić powiązanie testowe w module test w przypadku testów Robolectric lub w module androidTest w przypadku testów instrumentowanych. Zalecamy, aby w miarę możliwości używać @TestInstallIn.

Powiązywanie nowych wartości

Użyj adnotacji @BindValue, aby łatwo powiązać pola w teście z grafem zależności Hilt. Oznacz pole adnotacją @BindValue, a zostanie ono powiązane zadeklarowanym typem pola z kwalifikatorami, które są obecne w tym polu.

W przykładzie AnalyticsService możesz zastąpić AnalyticsService fałszywą wartością za pomocą @BindValue:

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

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

  ...
}

Upraszcza to zarówno zastępowanie powiązania, jak i odwoływanie się do niego w teście, ponieważ możesz wykonać obie te czynności jednocześnie.

@BindValue działa z kwalifikatorami i innymi adnotacjami testowymi. Jeśli na przykład używasz bibliotek testowych, takich jak Mockito, możesz użyć jej w teście Robolectric w ten sposób:

...
class SettingsScreenTest {
  ...

  @BindValue @ExampleQualifier @Mock
  lateinit var qualifiedVariable: ExampleCustomType

  // Robolectric tests here
}

Jeśli musisz dodać powiązanie wielokrotne, możesz użyć adnotacji @BindValueIntoSet i @BindValueIntoMap zamiast @BindValue. @BindValueIntoMap wymaga też oznaczenia pola adnotacją klucza mapy.

Przypadki szczególne

Hilt udostępnia też funkcje obsługujące niestandardowe przypadki użycia.

Aplikacja niestandardowa do testów

Jeśli nie możesz użyć HiltTestApplication, ponieważ aplikacja testowa musi rozszerzać inną aplikację, oznacz nową klasę lub interfejs adnotacją @CustomTestApplication, przekazując wartość klasy podstawowej, którą ma rozszerzać wygenerowana aplikacja Hilt.

@CustomTestApplication wygeneruje klasę Application gotową do testowania za pomocą Hilt, która rozszerza aplikację przekazaną jako parametr.

@CustomTestApplication(BaseApplication::class)
interface HiltTestApplication

W tym przykładzie Hilt generuje Application nazwaną HiltTestApplication_Application, która rozszerza klasę BaseApplication. Ogólnie rzecz biorąc, nazwa wygenerowanej aplikacji to nazwa klasy oznaczonej adnotacją z dopiskiem _Application. Musisz ustawić wygenerowaną aplikację testową Hilt tak, aby była uruchamiana w testach instrumentowanych lub testach Robolectric, zgodnie z opisem w sekcji Testuj aplikację.

Wiele obiektów TestRule w teście instrumentowanym

Testy interfejsu Compose łączą już HiltAndroidRule z regułą testu Compose, taką jak createAndroidComposeRule. Jeśli masz dodatkowe obiekty TestRule, upewnij się, że HiltAndroidRule jest uruchamiana jako pierwsza. Zadeklaruj kolejność wykonywania za pomocą atrybutu order w @Rule:

@HiltAndroidTest
class SettingsScreenTest {

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

  @get:Rule(order = 1)
  val composeRule = createAndroidComposeRule<HiltTestActivity>()

  @get:Rule(order = 2)
  val otherRule = SomeOtherRule()

  // UI tests here.
}

Możesz też opakować reguły za pomocą RuleChain, umieszczając HiltAndroidRule jako regułę zewnętrzną.

@HiltAndroidTest
class SettingsScreenTest {

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

  // UI tests here.
}

Używanie punktu wejścia, zanim komponent singleton będzie dostępny

Adnotacja @EarlyEntryPoint zapewnia możliwość wyjścia z sytuacji, gdy punkt wejścia Hilt musi zostać utworzony, zanim komponent singleton będzie dostępny w teście Hilt.

Więcej informacji o @EarlyEntryPoint znajdziesz w dokumentacji Hilt.

Dodatkowe materiały

Więcej informacji o testowaniu znajdziesz w tych dodatkowych materiałach:

Dokumentacja

Treści dotyczące widoków