Criando um ambiente de testes com Espresso, Hilt e Navigation

Configurando um projeto Android com Kotlin, Clean Architecture e componentes do Jetpack para testes de interface

William Zabot
Android Dev BR
Published in
6 min readApr 10, 2021

--

Recentemente, tive que realizar testes de UI em um projeto Android que utiliza linguagem Kotlin, Clean Architecture e injeção de dependência com Dagger Hilt. Eu já realizava testes com Espresso e Navigation, mas nunca havia adicionado o Hilt a essa conta e inicialmente pensei que seria algo trivial, mas percebi que pode ser muito complexo se não tivermos um passo-a-passo.

Para escrever testes unitários na JVM ou testes de UI com Espresso (ou outra biblioteca), temos que entender uma série de elementos. O primeiro passo é escolher quais as bibliotecas de teste você irá utilizar e quais dependências dela precisa inserir no seu Gradle.

Dependências necessárias

Inserir as dependências das bibliotecas é o primeiro passo para começar a criar um "ambiente de testes". Coloco entre aspas, pois não se trata de um ambiente de teste real, com virtualização ou contêineres, para os quais, geralmente essa nomenclatura é utilizada, mas sim do fato de tornar seu projeto testável utilizando as configurações corretas.

Em cada artigo, vídeo ou documentação, uma diferente dependência nos é apresentada e é necessário certificar-se de que você está realmente utilizando tudo que está no seu Gradle, ou se algumas coisas estão lá "só por estar". Uma forma de descobrir isto é analisando seus imports e ver quais os caminhos que estão sendo chamados para as anotações e métodos que as libs fornecem.

Espresso + Navigation + Hilt

Para utilizar e testar o Navigation Component, o Dagger Hilt e o Espresso no seu projeto, as seguintes libs são utilizadas:

No build.gradle do seu Project insira o classpath do Hilt e do Navigation Safe Args, que é uma forma segura de transmitir dados entre destinos fornecida pelo Navigation.

Adicione estes plugins no build.gradle do seu app.

Adicione todas essas dependências no build.gradle do seu app. Entre elas estão as dependências do Hilt, a integração do Hilt com a ViewModel, o Navigation e todas as dependências de teste, sendo o contrib opcional, pois serve para testar a RecyclerView, Intents e etc. A implementação específica de cada um destes componentes ou ideias para casos de teste não é o foco deste artigo, mas sim como fazer a configuração para os testes poderem rodar sem exceções que não tem relação nenhuma com as suas features.

Tendo as dependências inseridas e após ter feito o sync do Gradle, estamos prontos para usar tudo que essas bibliotecas nos tem para oferecer. Vamos começar por partes: imagine que não estivéssemos usando o Hilt no nosso projeto. Sem as dependências no Gradle ou qualquer injeção com o Hilt, bastaria criar uma classe assim no pacote androidTest para testar o Navigation Component:

Estamos criando uma classe de teste, instanciando uma Activity como uma rule e declarando um TestNavHostController como lateinit var. Logo abaixo, estamos usando a anotação @Before para fazer um setUp de nossas configurações que se resumem em: dar valor ao TestNavHostController, setar nosso "grapho" de navegação e instanciar um Fragment na tela através do que podemos chamar de "scenario".

Este "scenário", (cenário em português), consiste em uma Activity que nos é fornecida, onde podemos inflar nosso Fragment.

Assim, cada função anotada com @Test que você rodar irá passar pelo @Before que irá instanciar nosso fragment para podermos operar nossos testes nele. No corpo dos testes você pode utilizar a sintaxe comum do Espresso como onView, withId, perform, click(), entre outras. E nesse caso, testar se ao clicar no botão, o destino do seu navController corresponde com o que você escreveu na classe do Fragment testado.

Se fossemos usar essa config que podemos chamar de Espresso + Navigation, removeríamos todas dependências relacionadas ao Hilt (perderíamos um eficiente injetor de dependência) e adicionaríamos outras quatro dependências. Elas são responsáveis por criar um ambiente com o ActivityScenario padrão, coisa que iremos abolir com o Hilt, pois criaremos nossas próprias configurações de testes.

Tome cuidado para não duplicar dependências pois o JUnit já vem por padrão no seu projeto. E certifique-se de que as versões que vai utilizar estão atualizadas, pois updates são lançados de tempos em tempos.

Testando com o Hilt

Se tentarmos usar o mesmo código recém apresentado para testar o Navigation Component, com o Hilt inserido ao projeto, receberemos uma série de exceções. Isto porque, o Hilt usa a anotação @AndroidEntryPoint nos pontos de entrada do projeto, ou seja, Activities e Fragments.

A Activity padrão que nos é fornecida quando instanciamos a classe ActivityScenarioRule não possui essa anotação, então é necessário seguir três passos principais para contornar esta situação.

1. Criar um HiltTestRunner e adicionar ao Gradle

No pacote androidTest, você deve criar a seguinte classe:

E adicionar o caminho dela dentro do defaultConfig do build.gradle do seu app, substituindo o antigo testInstrumentationRunner:

2. Criar uma HiltTestActivity e adicionar ao Manifest.xml

Precisamos criar uma nova Activity que terá a anotação @AndroidEntryPoint (HiltTestActivity é só um nome didático para memorizar e aprender o processo, mas outro nome também pode ser utilizado). Porém, não queremos esta Activity de teste no nosso ambiente real, então vamos criar ela dentro do pacote debug.

Mude a exibição do seu projeto de Android para Project no Android Studio e você verá o diretório src, que contém os diretórios androidTest, test e main.

No diretório src você irá criar um pacote chamado debug. Dentro dele crie a pasta java, assim como existe nos outros diretórios. E dentro da pasta java crie os diretórios comuns, como no restante do seu app, por exemplo: com.williamzabot.events (o caminho que você escolhe na criação do projeto do Android Studio). Isso significa que o app foi desenvolvido por mim (ou alguém com mesmo nome rs) e provavelmente é um app que envolve eventos. E dentro do último diretório criado (nesse caso seria dentro de events), crie uma classe como:

Depois disto, copie o seu Manifest.xml e cole na raiz da pasta debug. Porém, vamos apagar o que não iremos utilizar e deixar apenas a estrutura. Ele deve ficar semelhante a este código:

Lembre-se de colocar a HiltTestActivity como exported = false. Isto garante que ela irá pertencer apenas ao debug.

O resultado final deve se parecer com isto:

3. Criar o seu launchFragmentInHiltContainer customizado

No mesmo diretório do seu HiltTestRunner (passo 1), crie um File chamado HiltExt. Dentro dele copie o seguinte código:

Créditos ao canal do Youtube do Philipp Lackner que forneceu esse código incrível!

No código acima você tem uma inline fun <reified T : Fragment>. O T (generics) representa o fragmento que iremos passar entre <> e o inline e o reified permitem que o T possa ser instanciado, como por exemplo: T::class.java. Em uma fun comum, o compilador não permitiria que T fosse instanciado.

Tendo feito todos os passos com sucesso, você já consegue fazer um setUp bem legível e poderá testar sua UI com Espresso, Hilt e Navigation. Por exemplo dessa forma:

Agora utilizamos o @HiltAndroidTest, ao invés do @RunWith(AndroidJUnit4ClassRunner::class). Iniciamos uma rule do Hilt e declaramos o nosso TestNavController e um evento que servirá como mock para testar se os argumentos estão sendo passados para o fragment com sucesso. Algo que devemos lembrar é que a key que passamos como primeiro parâmetro no bundleOf() deve ser EXATAMENTE igual ao nome que você deu para seu argument no grapho. Por exemplo:

A sintaxe runOnMainSync {} garante que o que estiver dentro das chaves executará na MainThread. Isso é necessário quando queremos setar o grapho de navegação ou navegar para outra tela.

Por fim, inflamos nosso fragmento e na função test_click_button() utilizo o mesmo exemplo de teste do currentDestination que poderia ser feito facilmente sem o Hilt. Porém, fazendo essas configurações temos acesso a qualquer um de nossos fragmentos e podemos inclusive, mocar objetos na classe de teste com os módulos do Hilt.

Espero ter ajudado!

--

--

William Zabot
Android Dev BR

Android Developer (Kotlin/Java), Jetpack, Dependency Injection.