Testar os Fragments do seu aplicativo

Este tópico descreve como incluir APIs fornecidas pelo framework em testes que avaliam o comportamento de cada fragmento.

Os fragmentos servem como contêineres reutilizáveis no seu app, permitindo que o mesmo layout da interface do usuário seja apresentado em diversas atividades e configurações de layout. Considerando a versatilidade dos fragmentos, é importante garantir que eles forneçam uma experiência consistente e eficiente em termos de recursos. Observe o seguinte:

  • O fragmento não pode depender de uma atividade mãe ou de um fragmento específicos.
  • Só crie uma hierarquia de visualização de fragmento se o fragmento estiver visível para o usuário.

Para ajudar a configurar as condições para a realização desses testes, a biblioteca fragment-testing do AndroidX fornece a classe FragmentScenario para criar fragmentos e mudar o Lifecycle.State deles.

Declarar dependências

Para usar FragmentScenario, defina o artefato fragment-testing-manifest no seu o arquivo build.gradle do app usando debugImplementation e o artefato fragment-testing usando androidTestImplementation, como mostrado no exemplo a seguir:

Groovy

dependencies {
    def fragment_version = "1.8.3"

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

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

Kotlin

dependencies {
    val fragment_version = "1.8.3"

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

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

Os exemplos de teste nesta página usam declarações das bibliotecas Espresso e Truth (link em inglês). Para mais informações sobre outras bibliotecas de declaração e testes disponíveis, consulte Configurar projetos para o AndroidX Test.

Criar um fragmento

O FragmentScenario inclui os seguintes métodos para iniciar fragmentos em testes:

  • launchInContainer(), para testar a interface do usuário de um fragmento. O FragmentScenario anexa o fragmento ao controlador de visualização raiz de uma atividade. Caso contrário, essa atividade de contenção fica vazia.
  • launch(), para testes sem a interface do usuário do fragmento. O FragmentScenario anexa esse tipo de fragmento a uma atividade vazia, que não tem uma visualização raiz.

Depois de iniciar um desses tipos de fragmento, o FragmentScenario orienta o fragmento em teste para um estado especificado. Por padrão, esse estado é RESUMED, mas é possível substituí-lo pelo argumento initialState. O estado RESUMED indica que o fragmento está em execução e visível para o usuário. É possível avaliar as informações sobre os elementos da interface usando os testes de interface do Espresso.

Os exemplos de código a seguir mostram como iniciar o fragmento usando cada método:

Exemplo 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)
        ...
    }
}

Exemplo 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)
        ...
    }
}

Fornecer dependências

Caso seus fragmentos tenham dependências, você pode oferecer versões de teste delas fornecendo um FragmentFactory personalizado aos métodos launchInContainer() ou launch().

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

Para ver mais informações sobre como usar FragmentFactory para fornecer dependências para fragmentos, consulte Gerenciador de fragmentos.

Orientar o fragmento para um novo estado

Nos testes de interface do seu app, geralmente é suficiente iniciar o fragmento em teste e começar a testagem de um estado RESUMED. Em testes de unidade mais refinados, no entanto, você também pode avaliar o comportamento do fragmento à medida que ele passa de um estado de ciclo de vida para outro. É possível especificar o estado inicial passando o argumento initialState para qualquer uma das funções launchFragment*().

Para orientar o fragmento para um estado de ciclo de vida diferente, chame moveToState(). Esse método é compatível com os seguintes estados como argumentos: CREATED, STARTED, RESUMED e DESTROYED. Ele simula uma situação em que o fragmento ou a atividade que contém o fragmento muda o estado por qualquer motivo.

O exemplo a seguir inicia um fragmento de teste no estado INITIALIZED e, em seguida, move-o para o 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.
        ...
    }
}

Recriar o fragmento

Se o app estiver sendo executado em um dispositivo com poucos recursos, o sistema poderá destruir a atividade que contém o fragmento. Essa situação exige que o app recrie o fragmento quando o usuário retorna a ele. Para simular essa situação, chame recreate():

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

FragmentScenario.recreate() destrói o fragmento e o host dele e depois os recria. Quando a classe FragmentScenario recria o fragmento em teste, ele retorna ao estado do ciclo de vida em que se encontrava antes de ser destruído.

Como interagir com fragmentos de interface

Para acionar ações de interface no seu fragmento em teste, use os matchers de visualização do Espresso para interagir com elementos na sua visualização:

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

Se você precisar chamar um método no próprio fragmento, como responder a uma seleção no menu de opções, faça isso de modo seguro com uma referência ao fragmento usando FragmentScenario.onFragment() e transmitindo uma FragmentAction:

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

Testar ações de caixas de diálogo

O FragmentScenario também é compatível com testes de fragmentos de caixas de diálogo. Embora os fragmentos de caixas de diálogo tenham elementos de interface, o layout é preenchido em uma janela separada, e não na atividade em si. Por isso, use FragmentScenario.launch() para testar fragmentos de caixas de diálogo.

O exemplo a seguir testa o processo de dispensa da caixa de 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())
    }
}