Android

13 out, 2016

Testes no Android com Espresso - Parte 02

Publicidade

espresso-1

Na parte 1, nós vimos como configurar nosso projeto. Se você quiser começar a partir desta parte, clone o projeto EspressoTests no GitHub e faça um fork da branch ‘part_1’, que representa o estado do projeto ao final da parte 1.

Criando o primeiro teste

Vamos começar testando a tela de login. Para isso, crie uma classe ‘LoginActivityTest’ no diretório ‘/app/src/androidTest/’ do projeto, conforme a imagem abaixo:

espresso-2

Anote a classe que criamos com a anotação:

@RunWith(AndroidJUnit4.class)

Isso irá indicar que essa classe deve ser executada com o AndroidJUnitRunner; se quiser saber mais sobre ele, acesse este link.

Agora, vamos definir nossa ActivityTestRule com a activity que será iniciada antes de todos os nossos testes, ou seja, a activity que queremos testar.

@Rule
public ActivityTestRule<LoginActivity>
    mActivityRule = new ActivityTestRule<>(LoginActivity.class, false, true);

Os parâmetros do construtor são:

  • A activity que será testada (LoginActivity.class);
  • initialTouchMode (false);
  • Deve-se iniciar automaticamente ou não (true).

Seu código deve estar desta maneira:

@RunWith(AndroidJUnit4.class)
public class LoginActivityTest {

  @Rule
  public ActivityTestRule<LoginActivity>
      mActivityRule = new ActivityTestRule<>(LoginActivity.class, false, true);
}

Escrevendo o primeiro teste

Agora vamos escrever nosso primeiro teste. Devemos testar os diferentes estados que a tela em questão pode assumir. Por exemplo, assim que iniciamos a LoginActivity, ela exibe uma imagem, dois campos de texto e um botão. Este pode ser considerado o estado inicial da tela. Vamos escrever um teste para verificar esse estado.

@Test
public void whenActivityIsLaunched_shouldDisplayInitialState() {
  onView(withId(R.id.login_image)).check(matches(isDisplayed()));
  onView(withId(R.id.login_username)).check(matches(isDisplayed()));
  onView(withId(R.id.login_password)).check(matches(isDisplayed()));
  onView(withId(R.id.login_button)).check(matches(isDisplayed()));
}

Uma das vantagens do Espresso é que a sintaxe é bem intuitiva. Praticamente podemos ler em linguagem natural o que o teste está executando. Vamos analisar a primeira linha do teste:

onView(withId(R.id.login_image)).check(matches(isDisplayed()));

Poderíamos ler essa linha em linguagem natural, por exemplo: “Verifique que a view com id login_image está visível na tela”. Agora, explicando um pouco o que está acontecendo:

  • onView() vai receber o ViewMatchers que passarmos como parâmetro e irá nos retornar um objeto ViewInteraction. Em outras palavras, estamos passando pra ele a view que queremos interagir;
  • withId() vai receber o id da view e irá nos retornar um ViewMatchers;
  • check() vai receber um ViewAssertion. Ou seja, vai verificar se é válida a asserção que estamos passando como parâmetro;
  • matches() vai receber um ViewMatchers e retornar um ViewAssertion;
  • isDisplayed() é o ViewMatchers que utilizaremos.

Para saber mais sobre os objetos, clique nos links sobre ViewMatchers, ViewInteraction e ViewAssertion.

Executando o teste

Agora é só executar o teste, certo? Errado! Precisamos desativar as animações nas opções de desenvolvedor do nosso emulador/device. Para fazer isso, vá em Configurações > Programador (ou opções de desenvolvedor) e desative estas três opções:

  • Animação em escala;
  • Escala de transição;
  • Escala de duração da animação.

O Espresso aguarda a UI Thread ficar ociosa (idle) para executar o próximo passo do teste. Porém, se as animações estiverem ligadas, ele irá se perder e os testes irão quebrar – leia mais sobre isso aqui.

Agora, para rodar este teste, basta clicar no botão direito do mouse sobre o método e depois clicar na opção Run ‘whenActivityIsLaunched…’. Se tudo correr bem, você deve ver o teste ser executado no seu emulador e o console de testes estará assim:

espresso

Certo, mas como eu garanto que o Espresso está realmente funcionando e olhando se minhas views estão aparecendo na tela? Para garantir isso, vamos alterar a primeira linha desse teste que fizemos:

onView(withId(R.id.login_image)).check(matches(not(isDisplayed())));

O método not() irá inverter o resultado que esperávamos. Então, estou dizendo nesse teste que a view com id login_image não estará visível na tela, o que não é verdade, pois, ao iniciar a tela, a imagem estará visível. Então, o teste deve obrigatoriamente falhar. Se rodarmos o teste novamente:

espresso-1

Ótimo, nosso teste falhou, o que significa que o Espresso está fazendo as asserções corretamente. Melhor que isso, repare no log que ele mostra:

Expected: not is displayed on the screen to the user
Got: AppCompatImageView{..., visibility=VISIBLE, ...}

Ou seja, ele também nos mostra onde o teste está falhando. Agora podemos remover o método not() que adicionamos e rodar esse teste novamente. Deu verde? Então vamos em frente.

Esse teste foi fácil, o que devemos fazer agora é testar os outros estados da tela. Um desses outros estados é quando deixamos um dos campos de texto em branco, e clicamos no botão de login. Quando isso acontece, o app exibe um dialog na tela. Vamos escrever um teste para verificar esse estado.

@Test
public void whenAnyEditTextIsEmpty_andClickOnLoginButton_shouldDisplayDialog() {
  onView(withId(R.id.login_username)).perform(typeText("admin"));
  onView(withId(R.id.login_button)).perform(click());
  onView(withText(R.string.validation_message)).check(matches(isDisplayed()));
  onView(withText(R.string.ok)).perform(click());

  onView(withId(R.id.login_username)).perform(typeText(""));
  onView(withId(R.id.login_password)).perform(typeText("pass123"));
  onView(withId(R.id.login_button)).perform(click());
  onView(withText(R.string.validation_message)).check(matches(isDisplayed()));
onView(withText(R.string.ok)).perform(click());

Reparem que utilizamos três novos métodos:

  • perform(): vai executar a ViewAction que receber como parâmetro;
  • typeText(): ViewAction para digitar um texto na view;
  • click(): ViewAction para clicar na view;
  • withText(): vai procurar a view que contenha o texto passado como parâmetro.

O primeiro bloco do nosso teste está executando as seguintes ações:

  • Escrevendo um texto no campo login_username;
  • Clicando no botão de login;
  • Verificando que o dialog está aparecendo (pois deixamos o campo senha em branco);
  • Clicando no Ok do dialog para fechá-lo.

O segundo bloco faz basicamente a mesma coisa que o primeiro, só que com o campo login_password. A única diferença é que ele limpa o campo login_username antes, pois o campo está preenchido por causa do bloco anterior. Isso não é muito legal, ter que limpar o que os passos anteriores do teste fizeram para executar os próximos passos. Nossos testes devem validar os cenários de maneira isolada. Então, vamos refatorar este código.

@Test
public void whenPasswordIsEmpty_andClickOnLoginButton_shouldDisplayDialog() {
  onView(withId(R.id.login_username)).perform(typeText("admin"));
  onView(withId(R.id.login_button)).perform(click());
  onView(withText(R.string.validation_message)).check(matches(isDisplayed()));
  onView(withText(R.string.ok)).perform(click());
}

@Test
public void whenUserNameIsEmpty_andClickOnLoginButton_shouldDisplayDialog() {
  onView(withId(R.id.login_password)).perform(typeText("pass123"));
  onView(withId(R.id.login_button)).perform(click());
  onView(withText(R.string.validation_message)).check(matches(isDisplayed()));
onView(withText(R.string.ok)).perform(click());

Ótimo, agora temos dois testes, e um não está influenciando no outro. Porém, ainda repetimos os mesmos passos para ambos os testes. Acho que podemos refatorá-los novamente.

@Test
public void whenPasswordIsEmpty_andClickOnLoginButton_shouldDisplayDialog() {
  testEmptyFieldState(R.id.login_username);
}

@Test
public void whenUserNameIsEmpty_andClickOnLoginButton_shouldDisplayDialog() {
  testEmptyFieldState(R.id.login_password);
}

private void testEmptyFieldState(int notEmptyFieldId){
  onView(withId(notEmptyFieldId)).perform(typeText("defaultText"));
  onView(withId(R.id.login_button)).perform(click());
  onView(withText(R.string.validation_message)).check(matches(isDisplayed()));
  onView(withText(R.string.ok)).perform(click());
}

Agora está melhor, temos um código mais limpo e reutilizável. Esse é um bom momento para eu dar a primeira dica desta série:

Faça testes pequenos, não faça testes longos.

Evite fazer testes que executem várias coisas, foque em cenários pequenos e, de preferência, independentes.

Tente rodar os testes agora. Passou? Na minha tentativa não passou, e já vou explicar o motivo. Vamos analisar o log primeiro:

Error performing 'single click - At Coordinates: ... on view 'with id: com.example.heitorcolangelo.espressotests:id/login_button'.

Aparentemente, o Espresso não conseguiu efetuar o click no botão de login, ou seja, o problema está na segunda linha do método testEmptyFieldState:

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

Coloque um breakpoint nessa linha e vamos ver a tela do emulador neste momento:

espresso-2

Repare que o teclado virtual está ocupando uma boa parte da tela, inclusive ele está cobrindo o nosso botão de login, por isso o Espresso não pode clicar no botão. Para corrigir esse problema, vamos utilizar o método closeSoftKeyboard().

onView(withId(notEmptyFieldId)).perform(typeText("defaultText"),
closeSoftKeyboard());

Esse método irá fechar o teclado virtual após digitarmos o texto “defaultText”. Nosso botão de login ficará visível e o Espresso conseguirá clicar nele. Rode o teste novamente, acredito que agora irá passar.

Aí vai a segunda dica:

Sempre que tiver um teste que envolva digitar texto em um EditText, não se esqueça de usar o método closeSoftKeyboard() para esconder o teclado virtual.

Se você deixar o teclado virtual aberto durante os testes, ele vai ficar por cima de views que podem ser essenciais para seu teste, e este irá falhar.

Ufa, bastante coisa até aqui, mas ainda temos dois cenários para validar:

  1. Quando os dois campos estão vazios, deve exibir o dialog;
  2. Quando os dois campos são preenchidos, deve abrir a MainActivity.

O cenário 1 vou deixar como exercício para você resolver. O cenário 2 envolve outro conceito que vou deixar para o próximo artigo.

Ao final desta etapa, o seu código deve estar parecido com o branch ‘part_2’.

Se tiver dúvidas, sugestões ou encontrou alguma informação errada neste artigo, deixe um comentário abaixo.