Проверьте свои фрагменты, проверьте свои фрагменты

В этом разделе описывается, как включать API-интерфейсы, предоставляемые платформой, в тесты, оценивающие поведение каждого фрагмента.

Фрагменты служат контейнерами многократного использования в вашем приложении, позволяя вам представлять один и тот же макет пользовательского интерфейса в различных действиях и конфигурациях макета. Учитывая универсальность фрагментов, важно убедиться, что они обеспечивают согласованность и эффективность использования ресурсов. Обратите внимание на следующее:

  • Ваш фрагмент не должен зависеть от конкретного родительского действия или фрагмента.
  • Не следует создавать иерархию представлений фрагмента, если фрагмент не виден пользователю.

Чтобы помочь настроить условия для выполнения этих тестов, библиотека fragment-testing AndroidX предоставляет класс FragmentScenario для создания фрагментов и изменения их Lifecycle.State .

Объявление зависимостей

Чтобы использовать FragmentScenario определите артефакт fragment-testing-manifest в файле build.gradle вашего приложения с помощью debugImplementation и артефакт fragment-testing с помощью androidTestImplementation , как показано в следующем примере:

классный

dependencies {
    def fragment_version = "1.8.2"

    debugImplementation "androidx.fragment:fragment-testing-manifest:$fragment_version"

    androidTestImplementation "androidx.fragment:fragment-testing:$fragment_version"
}

Котлин

dependencies {
    val fragment_version = "1.8.2"

    debugImplementation("androidx.fragment:fragment-testing-manifest:$fragment_version")

    androidTestImplementation("androidx.fragment:fragment-testing:$fragment_version")
}

В примерах тестирования на этой странице используются утверждения из библиотек Espresso и Truth . Информацию о других доступных библиотеках тестирования и утверждений см. в разделе Настройка проекта для AndroidX Test .

Создать фрагмент

FragmentScenario включает в себя следующие методы для запуска фрагментов в тестах:

  • launchInContainer() для тестирования пользовательского интерфейса фрагмента. FragmentScenario прикрепляет фрагмент к контроллеру корневого представления действия. В противном случае эта содержащая активность пуста.
  • launch() для тестирования без пользовательского интерфейса фрагмента. FragmentScenario прикрепляет этот тип фрагмента к пустой активности , не имеющей корневого представления.

После запуска одного из этих типов фрагментов FragmentScenario переводит тестируемый фрагмент в заданное состояние. По умолчанию это состояние RESUMED , но вы можете переопределить его с помощью аргумента initialState . Состояние RESUMED указывает, что фрагмент запущен и виден пользователю. Оценить информацию о его элементах пользовательского интерфейса можно с помощью тестов Espresso UI .

Следующие примеры кода показывают, как запустить фрагмент с помощью каждого метода:

пример запускаInContainer()

@RunWith(AndroidJUnit4::class)
class MyTestSuite {
    @Test fun testEventFragment() {
        // The "fragmentArgs" argument is optional.
        val fragmentArgs = bundleOf(selectedListItem to 0)
        val scenario = launchFragmentInContainer<EventFragment>(fragmentArgs)
        ...
    }
}

пример запуска()

@RunWith(AndroidJUnit4::class)
class MyTestSuite {
    @Test fun testEventFragment() {
        // The "fragmentArgs" arguments are optional.
        val fragmentArgs = bundleOf("numElements" to 0)
        val scenario = launchFragment<EventFragment>(fragmentArgs)
        ...
    }
}

Предоставление зависимостей

Если у ваших фрагментов есть зависимости, вы можете предоставить тестовые версии этих зависимостей, предоставив специальную FragmentFactory для методов launchInContainer() или launch() .

@RunWith(AndroidJUnit4::class)
class MyTestSuite {
    @Test fun testEventFragment() {
        val someDependency = TestDependency()
        launchFragmentInContainer {
            EventFragment(someDependency)
        }
        ...
    }
}

Дополнительные сведения об использовании FragmentFactory для предоставления зависимостей фрагментам см. в разделе Менеджер фрагментов .

Привести фрагмент в новое состояние

В тестах пользовательского интерфейса вашего приложения обычно достаточно запустить тестируемый фрагмент и начать его тестирование из состояния RESUMED . Однако в более детальных модульных тестах вы также можете оценить поведение фрагмента при его переходе из одного состояния жизненного цикла в другое. Вы можете указать начальное состояние, передав аргумент initialState любой из функций launchFragment*() .

Чтобы перевести фрагмент в другое состояние жизненного цикла, вызовите moveToState() . Этот метод поддерживает следующие состояния в качестве аргументов: CREATED , STARTED , RESUMED и DESTROYED . Этот метод имитирует ситуацию, когда фрагмент или действие, содержащее ваш фрагмент, по какой-либо причине меняет свое состояние.

В следующем примере тестовый фрагмент запускается в состоянии INITIALIZED , а затем перемещается в состояние RESUMED :

@RunWith(AndroidJUnit4::class)
class MyTestSuite {
    @Test fun testEventFragment() {
        val scenario = launchFragmentInContainer<EventFragment>(
            initialState = Lifecycle.State.INITIALIZED
        )
        // EventFragment has gone through onAttach(), but not onCreate().
        // Verify the initial state.
        scenario.moveToState(Lifecycle.State.RESUMED)
        // EventFragment moves to CREATED -> STARTED -> RESUMED.
        ...
    }
}

Воссоздать фрагмент

Если ваше приложение работает на устройстве с низким уровнем ресурсов, система может уничтожить активность, содержащую ваш фрагмент. В этой ситуации ваше приложение должно воссоздать фрагмент, когда пользователь вернется к нему. Чтобы смоделировать эту ситуацию, вызовите recreate() :

@RunWith(AndroidJUnit4::class)
class MyTestSuite {
    @Test fun testEventFragment() {
        val scenario = launchFragmentInContainer<EventFragment>()
        scenario.recreate()
        ...
    }
}

FragmentScenario.recreate() уничтожает фрагмент и его хост, а затем воссоздает их. Когда класс FragmentScenario воссоздает тестируемый фрагмент, он возвращается в состояние жизненного цикла, в котором он находился до уничтожения.

Взаимодействие с фрагментами пользовательского интерфейса

Чтобы инициировать действия пользовательского интерфейса в тестируемом фрагменте, используйте средства сопоставления представлений Espresso для взаимодействия с элементами в вашем представлении:

@RunWith(AndroidJUnit4::class)
class MyTestSuite {
    @Test fun testEventFragment() {
        val scenario = launchFragmentInContainer<EventFragment>()
        onView(withId(R.id.refresh)).perform(click())
        // Assert some expected behavior
        ...
    }
}

Если вам нужно вызвать метод самого фрагмента, например, чтобы ответить на выбор в меню параметров, вы можете сделать это безопасно, получив ссылку на фрагмент с помощью FragmentScenario.onFragment() и передав FragmentAction :

@RunWith(AndroidJUnit4::class)
class MyTestSuite {
    @Test fun testEventFragment() {
        val scenario = launchFragmentInContainer<EventFragment>()
        scenario.onFragment { fragment ->
            fragment.myInstanceMethod()
        }
    }
}

Действия диалогового окна тестирования

FragmentScenario также поддерживает тестирование фрагментов диалогов . Хотя фрагменты диалогов содержат элементы пользовательского интерфейса, их макет заполняется в отдельном окне, а не в самой активности. По этой причине используйте FragmentScenario.launch() для проверки фрагментов диалога.

В следующем примере тестируется процесс закрытия диалога:

@RunWith(AndroidJUnit4::class)
class MyTestSuite {
    @Test fun testDismissDialogFragment() {
        // Assumes that "MyDialogFragment" extends the DialogFragment class.
        with(launchFragment<MyDialogFragment>()) {
            onFragment { fragment ->
                assertThat(fragment.dialog).isNotNull()
                assertThat(fragment.requireDialog().isShowing).isTrue()
                fragment.dismiss()
                fragment.parentFragmentManager.executePendingTransactions()
                assertThat(fragment.dialog).isNull()
            }
        }

        // Assumes that the dialog had a button
        // containing the text "Cancel".
        onView(withText("Cancel")).check(doesNotExist())
    }
}