Tester vos fragments

Cet article explique comment inclure des API fournies par le framework dans des tests qui évaluent le comportement de chaque fragment.

Les fragments servent de conteneurs réutilisables dans votre application. Ils vous permettent de présenter la même mise en page dans l'interface utilisateur pour différentes activités et configurations de mise en page. Compte tenu de la polyvalence des fragments, il est important de vérifier qu'ils offrent une expérience cohérente et économe en ressources. Remarques :

  • Votre fragment ne doit pas dépendre d'une activité parente ou d'un fragment spécifique.
  • Vous ne devez créer la hiérarchie des affichages d'un fragment que s'il est visible par l'utilisateur.

Pour vous aider à configurer les conditions permettant d'effectuer ces tests, la bibliothèque fragment-testing AndroidX fournit la classe FragmentScenario permettant de créer des fragments et de modifier leur Lifecycle.State.

Déclarer des dépendances

Pour utiliser FragmentScenario, définissez l'artefact fragment-testing dans le fichier build.gradle de votre application à l'aide de debugImplementation, comme illustré dans l'exemple suivant :

Groovy

dependencies {
    def fragment_version = "1.6.2"

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

Kotlin

dependencies {
    val fragment_version = "1.6.2"

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

Les exemples de test de cette page utilisent des assertions des bibliothèques Espresso et Truth. Pour en savoir plus sur les autres bibliothèques de test et d'assertion disponibles, consultez la page Configurer un projet pour AndroidX Test.

Créer un fragment

FragmentScenario inclut les méthodes suivantes pour lancer des fragments dans les tests :

  • launchInContainer(), pour tester l'interface utilisateur d'un fragment. FragmentScenario associe le fragment au contrôleur de l'affichage racine d'une activité. Sinon, cette activité est vide.
  • launch(), pour effectuer des tests sans l'interface utilisateur du fragment. FragmentScenario associe ce type de fragment à une activité vide, qui n'a pas d'affichage racine.

Après avoir lancé l'un de ces types de fragment, FragmentScenario fait passer le fragment testé à un état spécifié. Par défaut, cet état est RESUMED, mais vous pouvez le remplacer avec l'argument initialState. L'état RESUMED indique que le fragment est en cours d'exécution et visible par l'utilisateur. Vous pouvez évaluer les informations sur les éléments de son UI à l'aide des tests de l'UI Espresso.

Les exemples de code suivants montrent comment lancer votre fragment à l'aide de chaque méthode :

Exemple launchInContainer()

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

Exemple launch()

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

Fournir des dépendances

Si vos fragments ont des dépendances, vous pouvez fournir des versions de test de ces dépendances avec un FragmentFactory personnalisé pour les méthodes launchInContainer() ou launch().

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

Pour en savoir plus sur l'utilisation de FragmentFactory pour fournir des dépendances aux fragments, consultez la page Gestionnaire de fragments.

Faire passer le fragment à un nouvel état

Dans les tests de l'interface utilisateur de votre application, il suffit généralement de lancer le fragment testé et de commencer à le tester à partir d'un état RESUMED. Toutefois, lors de tests unitaires plus précis, vous pouvez également évaluer le comportement du fragment lorsqu'il passe d'un état de cycle de vie à un autre. Vous pouvez spécifier l'état initial en transmettant l'argument initialState à n'importe quelle fonction launchFragment*().

Pour que le fragment passe à un état de cycle de vie différent, appelez moveToState(). Cette méthode accepte les états suivants en tant qu'arguments : CREATED, STARTED, RESUMED et DESTROYED. Cette méthode simule une situation dans laquelle le fragment ou l'activité contenant votre fragment change d'état pour une raison quelconque.

L'exemple suivant lance un fragment de test à l'état INITIALIZED, puis le fait passer à l'état 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.
        ...
    }
}

Recréer le fragment

Si votre application s'exécute sur un appareil à faible ressources, le système peut détruire l'activité contenant votre fragment. Cette situation nécessite que votre application recrée le fragment lorsque l'utilisateur y retourne. Pour simuler cette situation, appelez recreate() :

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

FragmentScenario.recreate() détruit le fragment et son hôte, puis les recrée. Lorsque la classe FragmentScenario recrée le fragment testé, il revient à l'état de cycle de vie dans lequel il se trouvait avant sa destruction.

Interagir avec les fragments de l'UI

Pour déclencher des actions de l'interface utilisateur dans votre fragment testé, utilisez les outils de mise en correspondance des affichages Espresso pour interagir avec les éléments de votre affichage :

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

Si vous devez appeler une méthode sur le fragment lui-même, par exemple pour répondre à une sélection dans le menu d'options, vous pouvez le faire en toute sécurité en obtenant une référence au fragment à l'aide de FragmentScenario.onFragment() et en transmettant un objet FragmentAction :

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

Tester les actions de la boîte de dialogue

FragmentScenario permet également de tester les fragments de boîtes de dialogue. Bien que les fragments de boîtes de dialogue comportent des éléments d'interface utilisateur, leur mise en page est insérée dans une fenêtre distincte, plutôt que dans l'activité elle-même. Pour cette raison, utilisez FragmentScenario.launch() pour tester les fragments de boîtes de dialogue.

L'exemple suivant teste le processus de fermeture de la boîte de dialogue :

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