Android

27 out, 2016

Testes no Android com Espresso - Parte 04

Publicidade

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.