Cómo probar tus fragmentos

En este tema, se describe cómo incluir las API proporcionadas por el framework en pruebas que evalúan el comportamiento de cada fragmento.

Los fragmentos sirven como contenedores reutilizables dentro de tu app, lo que te permite presentar el mismo diseño de interfaz de usuario en una variedad de actividades y configuraciones de diseño. Dada la versatilidad de esos fragmentos, es importante validar que proporcionen una experiencia coherente y eficiente en el uso de recursos. Ten en cuenta lo siguiente:

  • Tu fragmento no debe depender de una actividad superior ni de un fragmento superior específicos.
  • No debes crear la jerarquía de vistas de un fragmento, a menos que sea visible para el usuario.

A fin de ayudar a establecer las condiciones para realizar estas pruebas, la biblioteca fragment-testing de AndroidX proporciona la clase FragmentScenario a fin de crear fragmentos y cambiar su Lifecycle.State.

Cómo declarar dependencias

Para usar FragmentScenario, define el artefacto fragment-testing en el archivo build.gradle de tu app mediante debugImplementation, como se muestra en el siguiente ejemplo:

Groovy

dependencies {
    def fragment_version = "1.5.1"

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

Kotlin

dependencies {
    val fragment_version = "1.5.1"

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

Los ejemplos de prueba que se incluyen en este documento usan aserciones de las bibliotecas Espresso y Truth. Para obtener más información sobre otras bibliotecas de aserciones y pruebas disponibles, consulta Cómo configurar el proyecto para AndroidX Test.

Cómo crear un fragmento

FragmentScenario incluye los siguientes métodos para iniciar fragmentos en las pruebas:

  • launchInContainer(), que se utiliza para probar la interfaz de usuario de un fragmento. FragmentScenario conecta el fragmento al controlador de la vista raíz de una actividad. Por lo demás, esta actividad que lo contiene está vacía.
  • launch(), que se utiliza para realizar pruebas sin la interfaz de usuario del fragmento. FragmentScenario vincula este tipo de fragmento a una actividad vacía, una que no tiene una vista raíz.

Después de iniciar uno de estos tipos de fragmentos, FragmentScenario cambia el fragmento sometido a prueba a un estado específico. De forma predeterminada, este estado es RESUMED, pero puedes anularlo con el argumento initialState. RESUMED indica que el fragmento está en ejecución y que el usuario lo puede ver. Puedes evaluar la información sobre los elementos de la IU mediante las pruebas de la IU de Espresso.

En los siguientes ejemplos de código, se muestra cómo iniciar tu fragmento por medio de cada método:

Ejemplo de 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)
        ...
    }
}

Ejemplo de 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)
        ...
    }
}

Cómo proporcionar dependencias

Si tus fragmentos tienen dependencias, puedes proporcionar versiones de prueba para ellas suministrando una FragmentFactory personalizada a los métodos launchInContainer() o launch().

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

Si quieres obtener más información sobre el uso de FragmentFactory a fin de proporcionar dependencias a fragmentos, consulta el Administrador de fragmentos.

Cómo llevar al fragmento a un nuevo estado

En las pruebas de IU de tu app, por lo general, basta con iniciar el fragmento en prueba y comenzar a probarlo desde un estado RESUMED. Sin embargo, mediante pruebas de unidades más detalladas, también puedes evaluar el comportamiento del fragmento a medida que pasa de un estado de ciclo de vida a otro. Puedes especificar el estado inicial si pasas el argumento initialState a cualquiera de las funciones launchFragment*().

Para cambiar el estado de ciclo de vida de un fragmento, llama a moveToState(). Este método admite los siguientes estados como argumentos: CREATED, STARTED, RESUMED y DESTROYED. Este método simula una situación en la que el fragmento o la actividad que lo contiene cambia su estado por algún motivo.

En el siguiente ejemplo, se inicia un fragmento de prueba en el estado INITIALIZED y, luego, se lo pasa al estado 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.
        ...
    }
}

Cómo recrear el fragmento

Si tu app se ejecuta en un dispositivo que tiene pocos recursos, es posible que el sistema destruya la actividad que contiene el fragmento. Esta situación requiere que tu app vuelva a crear el fragmento cuando el usuario regrese a él. Para simular esta situación, llama a recreate():

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

FragmentScenario.recreate() destruye el fragmento y su host y, luego, los vuelve a crear. Cuando la clase FragmentScenario vuelve a crear el fragmento sometido a prueba, este regresa al estado de ciclo de vida en el que se encontraba antes de que se destruyera.

Cómo interactuar con fragmentos de la IU

A fin de activar acciones de la IU en tu fragmento en prueba, usa los comparadores de vista de Espresso para interactuar con los elementos de tu vista:

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

Si necesitas llamar a un método en el propio fragmento, como responder a una selección en el menú de opciones, puedes hacerlo de forma segura obteniendo una referencia al fragmento usando FragmentScenario.onFragment() y pasando una FragmentAction:

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

Acciones del diálogo de prueba

FragmentScenario también admite la prueba de fragmentos de diálogo. Si bien los fragmentos de diálogo tienen elementos de la IU, su diseño se propaga en una ventana independiente, en lugar de hacerlo en la actividad en sí. Por ese motivo, usa FragmentScenario.launch() a los efectos de probar fragmentos de diálogo.

En el siguiente ejemplo, se prueba el proceso de descarte del diálogo:

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