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

Основные понятия и реализация Jetpack Compose

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

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

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 в тестах, добавьте в проект зависимость hilt-android-testing:

Яркий

dependencies {
    // For Robolectric tests.
    testImplementation 'com.google.dagger:hilt-android-testing:2.57.1'
    // ...with Kotlin.
    kaptTest 'com.google.dagger:hilt-android-compiler:2.57.1'
    // ...with Java.
    testAnnotationProcessor 'com.google.dagger:hilt-android-compiler:2.57.1'

    // For instrumented tests.
    androidTestImplementation 'com.google.dagger:hilt-android-testing:2.57.1'
    // ...with Kotlin.
    kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.57.1'
    // ...with Java.
    androidTestAnnotationProcessor 'com.google.dagger:hilt-android-compiler:2.57.1'
}

Kotlin

dependencies {
    // For Robolectric tests.
    testImplementation("com.google.dagger:hilt-android-testing:2.57.1")
    // ...with Kotlin.
    kaptTest("com.google.dagger:hilt-android-compiler:2.57.1")
    // ...with Java.
    testAnnotationProcessor("com.google.dagger:hilt-android-compiler:2.57.1")

    // For instrumented tests.
    androidTestImplementation("com.google.dagger:hilt-android-testing:2.57.1")
    // ...with Kotlin.
    kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.57.1")
    // ...with Java.
    androidTestAnnotationProcessor("com.google.dagger:hilt-android-compiler:2.57.1")
}

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

Вы должны аннотировать любой тест пользовательского интерфейса, который использует Hilt, с помощью @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.
}

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

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

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

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

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

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

  1. Создайте собственный класс, расширяющий AndroidJUnitRunner, в папке androidTest.
  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, как описано в руководстве по инструментированному модульному тестированию. Убедитесь, что вы указали полный путь к классу:

Яркий

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 для тестирования уровня пользовательского интерфейса, то можете указать, какое приложение использовать, в файле robolectric.properties:

application = dagger.hilt.android.testing.HiltTestApplication

Вы также можете настроить приложение для каждого теста отдельно, используя аннотацию @Config 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.
}

Если вы используете плагин Android Gradle версии ниже 4.2, включите преобразование классов @AndroidEntryPoint в локальных модульных тестах, применив следующую конфигурацию в файле build.gradle вашего модуля:

Яркий

hilt {
    enableTransformForLocalTests = true
}

Kotlin

hilt {
    enableTransformForLocalTests = true
}

Дополнительную информацию о enableTransformForLocalTests можно найти в документации по Hilt.

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

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

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

Чтобы внедрить типы в тест, используйте @Inject для внедрения через поле. Чтобы указать Hilt заполнить поля @Inject, вызовите 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 в тестах, создайте новый модуль Hilt в папке test или androidTest с фиктивной зависимостью и добавьте к нему аннотацию @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
  );
}

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

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

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

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 из раздела выше. Вы также можете поместить тестовую привязку в модуль test для тестов Robolectric или в модуль androidTest для инструментальных тестов. Рекомендуем по возможности использовать @TestInstallIn.

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

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

В примере с AnalyticsService вы можете заменить AnalyticsService на поддельный объект, используя @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();

  ...
}

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

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

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

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

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

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

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

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

Kotlin

@CustomTestApplication(BaseApplication::class)
interface HiltTestApplication

Java

@CustomTestApplication(BaseApplication.class)
interface HiltTestApplication { }

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

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

Если в вашем тесте есть другие объекты TestRule, вы можете обеспечить совместную работу всех правил несколькими способами.

Вы можете объединить правила следующим образом:

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 выполняется первым. Укажите порядок выполнения с помощью атрибута order в аннотации @Rule. Это работает только в 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

Использовать launchFragmentInContainer из библиотеки androidx.fragment:fragment-testing с Hilt нельзя, поскольку он зависит от действия, не аннотированного с помощью @AndroidEntryPoint.

Используйте код launchFragmentInHiltContainer из репозитория architecture-samples на GitHub.