Руководство по тестированию Hilt

Одно из преимуществ использования фреймворков внедрения зависимостей, таких как Hilt, заключается в том, что они упрощают тестирование кода.

Модульные тесты

Hilt не требуется для модульного тестирования, поскольку при тестировании класса, использующего внедрение через конструктор, вам не нужно использовать Hilt для создания экземпляра этого класса. Вместо этого вы можете напрямую вызвать конструктор класса, передав в него поддельные или макетные зависимости, как если бы конструктор не был аннотирован:

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

То же самое относится к классам ViewModel, полученным путем вызова hiltViewModel() в ваших composable-функциях. В модульных тестах создавайте ViewModel напрямую с помощью поддельных объектов. Подробнее о том, как состояние передается из ViewModel в composable-функции, рассказывается в статьях Состояние и Jetpack Compose и Где поднимать состояние.

Сквозные тесты

Для интеграционных тестов Hilt внедряет зависимости так же, как и в рабочем коде. Тестирование с помощью Hilt не требует обслуживания, поскольку Hilt автоматически генерирует новый набор компонентов для каждого теста.

Добавление зависимостей для тестирования

Чтобы использовать Hilt в тестах, добавьте в проект зависимость 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")

}

Настройка тестирования интерфейса

Вы должны аннотировать любой тест пользовательского интерфейса, который использует Hilt, с помощью @HiltAndroidTest. Эта аннотация отвечает за создание компонентов Hilt для каждого теста.

Также необходимо добавить HiltAndroidRule в тестовый класс. Он управляет состоянием компонентов и используется для внедрения в тест:

@HiltAndroidTest
class SettingsScreenTest {

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

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

    // Compose UI tests here.
}

Далее в тесте нужно указать класс Application, который Hilt генерирует автоматически.

Чтобы Hilt мог внедрять зависимости, создайте пустое действие с названием HiltTestActivity в наборе исходных файлов androidTest и добавьте к нему аннотацию @AndroidEntryPoint. createAndroidComposeRule использует это действие в качестве хоста для вашего составного контента.

Тестовое приложение

Инструментальные тесты, в которых используется Hilt, должны выполняться в объекте Application, поддерживающем Hilt. Библиотека предоставляет HiltTestApplication для использования в тестах. Если для тестов требуется другое базовое приложение, ознакомьтесь с разделом Специальное приложение для тестов.

Вам нужно настроить тестовое приложение для запуска в инструментальных тестах или тестах Robolectric. Приведенные ниже инструкции не относятся к Hilt, а являются общими рекомендациями по указанию пользовательского приложения для запуска в тестах.

Как настроить тестовое приложение в инструментальных тестах

Чтобы использовать тестовое приложение Hilt в инструментальных тестах, необходимо настроить новый инструмент запуска тестов. Это позволит использовать Hilt для всех инструментальных тестов в проекте. Выполните следующие действия:

  1. Создайте собственный класс, расширяющий AndroidJUnitRunner, в папке androidTest.
  2. Переопределите функцию newApplication и передайте название сгенерированного тестового приложения 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)
    }
}

Затем настройте этот инструмент запуска тестов в файле Gradle, как описано в руководстве по инструментированному модульному тестированию. Убедитесь, что вы указали полный путь к классу:

android {
    defaultConfig {
        // Replace com.example.android.dagger with your class path.
        testInstrumentationRunner = "com.example.android.dagger.CustomTestRunner"
    }
}
Как настроить тестовое приложение в тестах Robolectric

Если вы используете Robolectric для тестирования уровня пользовательского интерфейса, то можете указать, какое приложение использовать, в файле robolectric.properties:

application = dagger.hilt.android.testing.HiltTestApplication

Вы также можете настроить приложение для каждого теста отдельно, используя аннотацию @Config Robolectric:

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

  @get:Rule
  var hiltRule = HiltAndroidRule(this)

  // Robolectric tests here.
}

Функции тестирования

Когда Hilt будет готов к использованию в тестах, вы сможете использовать несколько функций для настройки процесса тестирования.

Внедрение типов в тесты

Чтобы внедрить типы в тест, используйте @Inject для внедрения через поле. Чтобы указать Hilt заполнить поля @Inject, вызовите hiltRule.inject().

Пример инструментального теста:

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

Как заменить привязку

Если вам нужно внедрить поддельный или макетный экземпляр зависимости, вам нужно указать Hilt не использовать привязку, которая использовалась в рабочем коде, а вместо этого использовать другую. Чтобы заменить привязку, вам нужно заменить модуль, содержащий привязку, тестовым модулем, содержащим привязки, которые вы хотите использовать в тесте.

Например, предположим, что ваш рабочий код объявляет привязку для AnalyticsService следующим образом:

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

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

Чтобы заменить привязку AnalyticsService в тестах, создайте новый модуль Hilt в папке test или androidTest с фиктивной зависимостью и добавьте к нему аннотацию @TestInstallIn. Вместо этого во все тесты в этой папке внедряется поддельная зависимость.

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

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

Поскольку composable-функции обычно используют эти зависимости косвенно через ViewModel, полученную с помощью hiltViewModel(), достаточно заменить привязку в Hilt. Тестируемая composable-функция автоматически выбирает поддельный объект.

Как заменить привязку в одном тесте

Чтобы заменить привязку в одном тесте, а не во всех, удалите модуль Hilt из теста с помощью аннотации @UninstallModules и создайте в тесте новый тестовый модуль.

Продолжим пример с AnalyticsService из предыдущей версии. Сначала укажите Hilt игнорировать рабочий модуль, используя аннотацию @UninstallModules в тестовом классе:

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

Затем необходимо заменить привязку. Создайте новый модуль в тестовом классе, который определяет тестовую привязку:

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

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

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

  // ...
}

Это заменяет привязку только для одного тестового класса. Если вы хотите заменить привязку для всех тестовых классов, используйте аннотацию @TestInstallIn из раздела выше. Вы также можете поместить тестовую привязку в модуль test для тестов Robolectric или в модуль androidTest для инструментальных тестов. Рекомендуем по возможности использовать @TestInstallIn.

Привязка новых значений

Используйте аннотацию @BindValue, чтобы легко привязать поля в тесте к графу зависимостей Hilt. Добавьте к полю аннотацию @BindValue, и оно будет связано с объявленным типом поля и всеми квалификаторами, которые для него заданы.

В примере с AnalyticsService вы можете заменить AnalyticsService на поддельный объект, используя @BindValue:

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

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

  ...
}

Это упрощает как замену привязки, так и ссылку на привязку в вашем тесте, позволяя вам делать и то, и другое одновременно.

@BindValue работает с квалификаторами и другими аннотациями тестирования. Например, если вы используете библиотеки тестирования, такие как Mockito, вы можете использовать их в тесте Robolectric следующим образом:

...
class SettingsScreenTest {
  ...

  @BindValue @ExampleQualifier @Mock
  lateinit var qualifiedVariable: ExampleCustomType

  // Robolectric tests here
}

Если вам нужно добавить мультипривязку, вы можете использовать аннотации @BindValueIntoSet и @BindValueIntoMap вместо @BindValue. @BindValueIntoMap требует, чтобы вы также аннотировали поле с помощью аннотации ключа карты.

Особые случаи

Hilt также предоставляет функции для поддержки нестандартных сценариев использования.

Собственное приложение для тестирования

Если вы не можете использовать HiltTestApplication, потому что тестовое приложение должно расширять другое приложение, добавьте аннотацию @CustomTestApplication к новому классу или интерфейсу, передав значение базового класса, который должно расширять сгенерированное приложение Hilt.

@CustomTestApplication сгенерирует класс Application, готовый к тестированию с помощью Hilt, который расширяет приложение, переданное в качестве параметра.

@CustomTestApplication(BaseApplication::class)
interface HiltTestApplication

В примере Hilt генерирует Application с именем HiltTestApplication_Application, который расширяет класс BaseApplication. Как правило, название сгенерированного приложения – это название аннотированного класса с добавлением _Application. Вам нужно настроить запуск сгенерированного тестового приложения Hilt в инструментальных тестах или тестах Robolectric, как описано в разделе Тестовое приложение.

Несколько объектов TestRule в инструментальном тесте

Тесты интерфейса Compose уже объединяют HiltAndroidRule с правилом тестирования Compose, например createAndroidComposeRule. Если у вас есть дополнительные объекты TestRule, убедитесь, что HiltAndroidRule выполняется первым. Укажите порядок выполнения с помощью атрибута order в элементе @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.
}

Вы также можете обернуть правила в RuleChain, поместив HiltAndroidRule в качестве внешнего правила.

@HiltAndroidTest
class SettingsScreenTest {

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

  // UI tests here.
}

Использовать точку входа до того, как станет доступен компонент-одиночка.

Аннотация @EarlyEntryPoint позволяет создать точку входа Hilt до того, как в тесте Hilt станет доступен компонент-одиночка.

Подробнее о @EarlyEntryPoint рассказывается в документации по Hilt.

Дополнительные ресурсы

Дополнительную информацию о тестировании можно найти в следующих ресурсах:

Документация

Просмотр контента