Dev (Back & Front)ARTIGO

Testes no Android com Espresso - Parte 04

No artigo anterior, aprendemos como mockar as intents do Android. Neste, conheça cenários da MainActivity.

espresso

No artigo anterior, aprendemos como mockar as intents do Android. Caso queira iniciar a partir desse texto, utilize o branch ‘part_3’ do projeto.

Cenários da MainActivity

A MainActivity possui a lista com os usuários que recebemos da API. Caso ocorra um erro na requisição, a MainActivity exibirá uma tela com um erro em vez da lista, e enquanto nenhuma resposta volta da API, um loading permanece na tela. Então, a princípio, temos três cenários para testar:

  1. Quando a requisição é feita com sucesso, devemos ver a lista com os usuários;
  2. Quando houver um erro na requisição, devemos ver a tela de erro;
  3. Quando não recebemos nenhuma resposta da API, devemos ver a tela de loading.

Ótimo, levantamos os primeiros estados para testarmos nossa activity. Porém, temos um problema com o terceiro cenário.

Nos testes que fizemos até aqui, o Espresso fez várias interações com o nosso app, mas já parou para pensar como o Espresso sabe a hora em que pode interagir com o app? Afinal de contas, os apps possuem animações e, em muitos casos, a UI só pode ser utilizada quando a animação acaba. Pois é, o Espresso faz exatamente isso, ele espera a UI Thread da aplicação ficar ociosa -  em inglês, idle. Enquanto a UI não termina as animações, o Espresso não interage com ela. Se o app não ficar idle em 60 segundos, o Espresso devolve um erro:

AppNotIdleException: Looped for 3544 iterations over 60 SECONDS. The following Idle Conditions failed .

Sabendo disso, você já consegue imaginar que o teste do terceiro cenário não vai funcionar, pois como a tela de loading é uma animação infinita, a UI Thread nunca vai ficar idle, e o Espresso nunca vai interagir com ela. Então, vamos eliminar esse cenário com esta dica muito importante e que vai te poupar muita dor de cabeça:

Cuidado com animações de ‘loading’ na tela. Se elas estiverem na tela, o seu teste com certeza irá falhar.

Escrevendo os testes

Vamos criar nossos testes para atender aos dois primeiros cenários. Vou deixar a configuração inicial da classe MainActivityTest por sua conta; ela é idêntica à configuração inicial da LoginActivityTest, a única diferença é no último parâmetro para criar a ActivityTestRule. Na LoginActivityTest usamos, true; nesta, iremos usar false. Lembra pra que serve esse parâmetro? Ele indica se a activity deve ser iniciada automaticamente.

Ao final dessa configuração, sua classe deve estar desta maneira:

@RunWith(AndroidJUnit4.class)
public class MainActivityTest {

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

}

Ok, poderíamos simplesmente sair escrevendo os testes da mesma maneira que fizemos na LoginActivityTest. Porém, como essa activity faz uma requisição para a API logo que é iniciada, dependemos do resultado da requisição para que o teste dê certo ou não. Isso é ruim, como eu havia dito, devemos manter nossos testes isolados. Vamos então isolar nosso teste mockando o resultado da requisição que o app faz para a API. Para isso, vamos utilizar a lib MockWebServer.

Configurando MockWebServer

Adicione a seguinte dependência ao seu arquivo build.gradle:

androidTestCompile 
"com.squareup.okhttp3:mockwebserver:$okHttpVersion"

Sincronize o projeto e vamos configurar o MockWebServer na nossa MainActivityTest. Dê uma olhada na implementação abaixo:

@RunWith(AndroidJUnit4.class)
public class MainActivityTest {

  private MockWebServer server;
  
  @Rule
  public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false);
  
  @Before
  public void setUp() throws Exception {
      server = new MockWebServer();
      server.start();
  }
  
  @After
  public void tearDown() throws IOException {
      server.shutdown();
  }
}

Declaramos dois novos métodos: setUp e tearDown. Anotamos esses métodos com Before e After, respectivamente. Os métodos anotados com Before serão executados antes de a activity ser iniciada. Os métodos anotados com After serão executados ao final de cada teste. Fora isso, nada de muito especial, estamos apenas criando uma nova instância de MockWebServer e dando um start no server.

Porém, nosso app ainda está apontando para a url real da API, ou seja:

http://api.randomuser.me/

Isso significa que, se escrevermos um teste, nosso app ainda irá fazer a requisição para a API real, e não para nosso MockWebServer. Porém, pela nossa implementação, a url é definida como uma variável no nosso arquivo build.gradle, o que torna essa redefinição de URL um pouco mais difícil. Para facilitar nossa vida, vamos usar a lib Mirror.

Importante: antes de continuar, revise como o app está fazendo as requisições para a API. Isso vai te ajudar a entender o que vamos fazer daqui pra frente.

Adicione esta dependência ao seu arquivo build.gradle:

androidTestCompile "net.vidageek:mirror:1.6.1"

Agora vamos criar os métodos para alterar a url da nossa classe UsersApi; vou mostrar o código e explicar linha a linha.

@RunWith(AndroidJUnit4.class)
public class MainActivityTest {

  private MockWebServer server;
  
  @Rule
  public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false);
  
  @Before
  public void setUp() throws Exception {
      server = new MockWebServer();
      server.start();
      setupServerUrl();
  }
  
  @After
  public void tearDown() throws IOException {
      server.shutdown();
  }
  
  private void setupServerUrl() {
      String url = server.url("/").toString();
      
      HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
      interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
      
      OkHttpClient client = new OkHttpClient.Builder().addInterceptor(interceptor).build();
      
      final UsersApi usersApi = UsersApi.getInstance();
      
      final Api api = new Retrofit.Builder()
            .baseUrl(url)
            .addConverterFactory(GsonConverterFactory.create(UsersApi.GSON))
            .client(client)
            .build()
            .create(Api.class);
            
      setField(usersApi, "api", api);
  }
  
  private void setField(Object target, String fieldName, Object value) {
      new Mirror()
              .on(target)
              .set()
              .field(fieldName)
              .withValue(value);
  }
}

Na linha 13, chamamos o método setupServerUrl(). Vamos analisá-lo:

  • Linha 22: definimos a url do nosso MockWebServer para “/”, pegamos o valor total da url e armazenamos na string url;
  • Linha 24 e 25: apenas definimos o nível de log do nosso objeto HttpLogginInterceptor;
  • Linha 27: criamos um objeto OkHttpClient e passamos o nosso interceptor;
  • Linha 29: referenciamos a instância de UsersApi;
  • Linha 31: criamos um novo objeto Api com o retrofit, só que dessa vez passando a url do nosso MockWebServer;
  • Linha 38: chamamos o método setField, que recebe como parâmetros:
    1 – O target, que é o objeto que terá o field alterado usando reflection com o mirror;
    2 – O nome do field que será alterado;
    3 – O novo valor que será atribuído ao field alterado.

O método setField é onde de fato usaremos a lib Mirror. Acredito que seja bem simples entender o que ele faz: basicamente estamos falando para o Mirror pegar o field “api” do nosso target (que é um objeto da classe UsersApi) e alterar o valor desse field para o objeto que passamos no parâmetro value. Desse modo, estamos alterando a url base do nosso projeto para apontar para o nosso MockWebServer.

É importante ressaltar que, se sua implementação permitir definir a url do endpoint de maneira mais simples, é bem provável que você não precise usar reflection.

Se você ficou com alguma dúvida, retome os passos anteriores antes de prosseguir; se, mesmo assim, estiver com dúvida, deixe nos comentários para que eu possa ajudar.

Agora, nosso teste está isolando nosso app das chamadas da API. Porém, ainda não definimos a resposta que nosso MockWebServer irá retornar para cada caso de teste.

Mockando o retorno da request

Para podermos mockar o retorno, devemos ter um mock do objeto json igual ao que a API retorna. Isso é simples, basta simularmos uma requisição para a API no nosso navegador, por exemplo:

http://api.randomuser.me/?results=20

Então, é só você copiar o json que aparecer no navegador e colocar em uma string em um lugar acessível aos testes. Eu costumo colocar em uma interface, conforme fiz no arquivo Mocks.java.

Agora vamos escrever nosso primeiro teste para a MainActivity. Neste teste, iremos verificar se, quando a API retorna com sucesso os usuários, nós visualizamos a lista de usuários na tela.

@Test
public void whenResultIsOk_shouldDisplayListWithUsers() {
  server.enqueue(new MockResponse().setResponseCode(200).setBody(Mocks.SUCCESS));
  mActivityRule.launchActivity(new Intent());
  onView(withId(R.id.recycler_view)).check(matches(isDisplayed()));
}

Linha 3: estamos chamando o método enqueue do MockWebServer. Esse método vai enfileirar (enqueue, em inglês) o objeto MockResponse que passamos como parâmetro. Estamos definindo o responseCode como 200, ou seja, uma requisição com sucesso. Também estamos definindo o body com o mock que acabamos de copiar e colocamos na interface Mocks.java. Resumindo: estamos dizendo para o MockWebServer: “Quando chegar uma requisição para você, retorne esse MockResponse”;

Linha 4: iniciamos a activity, passamos uma intent simples, uma vez que não é necessário nenhum extra nessa activity;

Linha 5: estamos verificando se nossa lista está visível.

Rode o teste, ele deve passar. Agora veja como ficou o log:

D/OkHttp: --> GET http://localhost:41183/?page=0&results=20 http/1.1
D/OkHttp: --> END GET
D/OkHttp: <-- 200 OK http://localhost:41183/?page=0&results=20 (34ms)ando

Veja que agora nosso endpoint é o localhost, ou seja, o MockWebServer.

Conseguimos testar um cenário da nossa activity de maneira isolada. Mas ainda falta outro cenário, aquele em que quando uma requisição falha (código entre 400 e 500), aparece a tela de erro. Vou deixar esse teste para você implementar. Ele é bem simples, você apenas terá que mudar o código de retorno e o body.

Se algo deu errado, retome os passos anteriores ou deixe um comentário abaixo para eu poder te ajudar. Ao final desta etapa (após ter implementado o segundo cenário de teste), seu código deve estar parecido com o da branch ‘part_4’.

Se tiver alguma dúvida, sugestão, ou se encontrou um erro no artigo, deixe um comentário.

Ainda precisamos testar se o layout que definimos para o nosso item da recycler view está sendo apresentado corretamente. Também precisamos verificar se, ao clicar em um item, enviamos as informações corretas para a activity de detalhes. Esses serão nossos próximos cenários de teste.

é desenvolvedor Android na Concrete Solutions, é graduado em Ciência da Computação pela UNESP de Presidente Prudente. No tempo livre, curte aprender música, viajar e sair com os amigos.

Ver perfil