Android

14 nov, 2016

Espresso Intents: não é magia, é tecnologia!

Publicidade

Se você leu meu artigo sobre Testes unitários vs aceitação, já ficou bem claro como é difícil controlar o ambiente de teste.

Existem diversas APIs do próprio Espresso, como ActivityMonitor e IdlingResources, que nos ajudam nessa empreitada. Utilizando esses dois recursos, já conseguimos criar excelentes testes de aceitação que nos salvam a vida.

Mas nem tudo é mar de rosas. Imagine um cenário como este:

1-nuqgrgz1ipslhzxagmhdgw

Como iremos voltar para nosso app? A não ser que criemos uma implementação da câmera com a “casca” do seu device, fica bem chato testar esse cenário.

E agora, Sica, quem poderá nos ajudar?

É exatamente sobre isso que esse artigo fala: o herói Espresso Intents.

O problema

Um dos principais pilares do nosso querido Android são as Intents. Como o nome diz, uma Intent é quando você tem a intenção (olha só) de fazer algo: um objeto de mensagem que pode ser usado para solicitar uma ação de outro componente de uma aplicação.

Por exemplo, para abrir a câmera, precisamos enviar uma Intent solicitando o app de câmera que o usuário tem no aparelho:

public void navigateToCamera(Uri photoLocalUri, int requestImageCapture) {
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, photoLocalUri);
    activity.startActivityForResult(intent, requestImageCapture);
}

Há três casos de uso fundamentais na utilização das Intents: para iniciar sua Activity, Service ou enviar um Broadcast. Ou seja, para iniciar (ou utilizar) qualquer coisa, você irá precisar de uma Intent e seus devidos extras (Bundle).

Precisamos repetir esse processo o tempo todo: seja para utilizar aplicações do próprio device ou até para aplicativos de terceiros.

No ambiente de teste

Só de ler o parágrafo acima, você já deve ter pensado duas vezes em implementar um teste na sua classe que utiliza esses recursos, né?

Bom, já que qualquer ação para inicializar algo necessita de uma Intent, fica muito difícil você ter certeza, nos seus testes, que as coisas estão acontecendo bem. E é exatamente isso que o Espresso Intents resolve: você consegue controlar todas as Intents que são disparadas (ou recebidas) pela sua aplicação.

Como funciona

O Espresso Intents é uma extensão do Espresso que possibilita você validar e fazer stubbings das Intents que são enviadas e recebidas pela sua aplicação dentro do ambiente de teste.

Sabe o Mockito? Então, é tipo isso, só que pra Intents.

Só de ser possível comparar com o Mockito, já mostra como essa API é valiosa para nossos testes. Vamos entender a sua estrutura:

Adeus, ActivityTestRule. Olá, IntentsTestRule.

Para iniciar nossa Activity, dentro de um teste anotado, declaramos nossa ActivityTestRule como uma Rule do JUnit e podemos iniciar qualquer Activity que desejamos:

@Rule
public ActivityTestRule activityRule = new ActivityTestRule<>(
    MainActivity.class,
    true,    // initialTouchMode
false); // launchActivity. False to set intent per method

Para inicializar o Espresso Intents, você precisa inicializar a API e liberá-la após o uso:

@Test
public void someTest() {
    Intents.init();
    
    onView(withId(R.id.some_id)).perform(click());

    Intents.release();
}

Mas, pra mim, é bem chato lembrar de fazer isso em todo teste.

Para a nossa alegria, existe a IntentsTestRule, que herda da ActivityTestRule.

A IntentsTestRule, por sua vez, facilita as coisas para a API do Espresso Intents funcionar. Ela inicia o Espresso Intents antes de cada teste anotado com nossa famosa anotação @Test e, quando esse teste terminar, ele irá finalizar a execução de toda a API.

Tá, mas o que isso faz?

Basicamente te torna um mágico das Intents, conseguindo validá-las e controlá-las como um ótimo Merlin.

1-1finvspkbhzr3njduu3vwgVocê controlando suas Intents

Stubbing de Intent

Agora que você é o Merlin, você tem o poder (lê-se algum programador facilitou isso pra você) de gravar todas as Intents que irão iniciar alguma Activity dentro da sua aplicação.

Já que você tem todas elas, o Espresso Intents tem dois métodos que fazem toda essa mágica.

Utilizando o mesmo exemplo de abrir a câmera, você consegue utilizar o método intending para retornar um resultado desejado:

@Test
public void shouldSelectImageOnCamera() {
    Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, data);

    intending(hasAction(MediaStore.ACTION_IMAGE_CAPTURE)).respondWith(result);

    onView(withId(R.id.camera)).perform(click());
}

Validando uma Intent

Nesse caso, você estava recebendo uma Intent.

Agora, avalie esta situação:

public void shareImage(String text, Uri imageUri) {
    Intent shareIntent = new Intent();
    shareIntent.setAction(Intent.ACTION_SEND);
    shareIntent.putExtra(Intent.EXTRA_STREAM, imageUri);
    shareIntent.setType("image/*");
    activity.startActivity(Intent.createChooser(shareIntent, text));
}

Se você deixar essa sua Intent vazar, você irá sair do contexto da sua aplicação, onde seus poderes de Merlin surtem efeito, e entrar no ambiente do nosso amiguinho Android, e para interagir com qualquer elemento da tela você irá precisar do UI-Automator. Só de pensar nesse teste, eu já abri a boca de sono.

Mas como iremos testar esse cenário?

Aí que temos o outro método mágico, o intended. Com ele, validar alguma Intent, e a combinando com o método intending, você é capaz de de isolar totalmente seu ambiente de teste:

@Test
public void shouldShareImageFromGallery() {
    Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, data);

    intending(hasAction(Intent.ACTION_CHOOSER)).respondWith(result);

    onView(withId(R.id.share)).perform(click());

    intended(allOf(
            hasExtras(allOf(
                    hasEntry(equalTo(Intent.EXTRA_INTENT), hasAction(Intent.ACTION_SEND)),
                    hasEntry(equalTo(Intent.EXTRA_INTENT), hasType("image/*")),
                    hasEntry(equalTo(Intent.EXTRA_TITLE), containsString("Share image")
            ),
            hasAction(equalTo(Intent.ACTION_CHOOSER))));
}

Agora sim, dá pra testar.

Limitações

No dia a dia, sinto muito a falta de conseguir interceptar uma Intent para abrir outra Activity, e modificar o conteúdo dentro dela, por exemplo:

public static Intent getCallingIntent(Context context, ArrayList<GalleryPhoto> imagesOnDevice) {
    Intent intent = new Intent(context, GalleryActivity.class);
    intent.putParcelableArrayListExtra(EXTRA_IMAGES_ON_DEVICE, imagesOnDevice);
    return intent;
}

...

@Test
public void someTest() {
	Instrumentation.ActivityResult result = getActivityResult(dataFakeForActivity);

    intending(hasExtra(GalleryActivity.EXTRA_IMAGES_ON_DEVICE)).respondWith(result);

    onView(withId(R.id.gallery)).perform(click());
}

Isso seria ótimo, já que em um ambiente de CI (emulador) não há imagem alguma. Ou seja, você precisa fornecer essas imagens de alguma maneira.

Mas, atualmente, isso não é suportado, o que nos obriga a fazer uma implementação menos “elegante” .

Projeto de exemplo

Calma que todo esse código não vai ficar só em um artigo. Criei esse projeto de exemplo que utiliza algumas situações interessantes e como você pode viajar no uso dessa API.

https://github.com/rsicarelli/Espresso-Intents

Conseguiu implementar algo legal? Não hesite em me mandar um pull request!

Sucesso

1-kehlrqqfzg0wifypazncwa

Aposto que seus testes irão fluir muito melhor com essa extensão maravilhosa do Espresso.

Mas não pense que seus poderes acabaram por aqui: no próximo artigo, irei falar sobre o vasto reino da Web e como interagir com ela.

Não esqueça de compartilhar, comentar ou acrescentar em algo nesse post!

Também, me segue lá no Twitter (@rsicarelli)!

Valeu!

Links

***

Artigo publicado originalmente em https://medium.com/android-dev-br/espresso-intents-n%C3%A3o-%C3%A9-m%C3%A1gica-%C3%A9-tecnologia-1fcfc8f21d3b#.dbib7fqgh.