Operações Assíncronas

Neste capítulo, aprenderemos como testar operações assíncronas usando Espresso Idling Resources.

Um dos desafios do aplicativo moderno é fornecer uma experiência de usuário tranquila. Fornecer uma experiência de usuário tranquila envolve muito trabalho em segundo plano para garantir que o processo do aplicativo não leve mais do que alguns milissegundos. A tarefa em segundo plano varia de uma tarefa simples a uma tarefa cara e complexa de buscar dados de API / banco de dados remoto. Para enfrentar o desafio do passado, um desenvolvedor costumava escrever tarefas caras e de longa execução em um thread de segundo plano e sincronizar com o UIThread principal quando o thread de segundo plano era concluído.

Se desenvolver um aplicativo multithread é complexo, escrever casos de teste para ele é ainda mais complexo. Por exemplo, não devemos testar um AdapterView antes de os dados necessários serem carregados do banco de dados. Se a busca dos dados for feita em um thread separado, o teste precisará aguardar até que o thread seja concluído. Portanto, o ambiente de teste deve ser sincronizado entre o thread de segundo plano e o thread de IU. O Espresso fornece um excelente suporte para testar o aplicativo multithread. Um aplicativo usa thread das seguintes maneiras e o espresso oferece suporte a todos os cenários.

Threading da interface do usuário

É usado internamente pelo Android SDK para fornecer uma experiência de usuário tranquila com elementos complexos da interface do usuário. O Espresso oferece suporte a esse cenário de forma transparente e não precisa de nenhuma configuração e codificação especial.

Tarefa assíncrona

Linguagens de programação modernas oferecem suporte à programação assíncrona para fazer threads leves sem a complexidade da programação de threads. A tarefa assíncrona também é suportada de forma transparente pela estrutura do espresso.

Tópico do usuário

Um desenvolvedor pode iniciar um novo thread para buscar dados complexos ou grandes do banco de dados. Para oferecer suporte a esse cenário, o espresso fornece o conceito de recurso inativo.

Vamos aprender o conceito de recurso ocioso e como fazê-lo neste capítulo.

visão global

O conceito de recurso ocioso é muito simples e intuitivo. A ideia básica é criar uma variável (valor booleano) sempre que um processo de longa execução for iniciado em uma thread separada para identificar se o processo está em execução ou não e registrá-lo no ambiente de teste. Durante o teste, o executor de teste verificará a variável registrada, se houver alguma encontrada, e então encontrará seu status de execução. Se o status de execução for verdadeiro, o executor de teste aguardará até que o status se torne falso.

O Espresso fornece uma interface, IdlingResources, com o objetivo de manter o status de execução. O principal método de implementação é isIdleNow (). Se isIdleNow () retornar verdadeiro, o espresso irá retomar o processo de teste ou então esperará até que isIdleNow () retorne falso. Precisamos implementar IdlingResources e usar a classe derivada. O Espresso também fornece algumas das implementações internas de IdlingResources para facilitar nossa carga de trabalho. Eles são os seguintes,

CountingIdlingResource

Isso mantém um contador interno de tarefas em execução. Ele expõe os métodos increment () e decrement () . increment () adiciona um ao contador e decrement () remove um do contador. isIdleNow () retorna verdadeiro somente quando nenhuma tarefa está ativa.

UriIdlingResource

Isso é semelhante ao CounintIdlingResource, exceto que o contador precisa ser zero por um período prolongado para levar a latência da rede também.

IdlingThreadPoolExecutor

Esta é uma implementação personalizada de ThreadPoolExecutor para manter o número de tarefas em execução ativas no pool de threads atual.

IdlingScheduledThreadPoolExecutor

Isso é semelhante a IdlingThreadPoolExecutor , mas também agenda uma tarefa e uma implementação personalizada de ScheduledThreadPoolExecutor.

Se qualquer uma das implementações acima de IdlingResources ou um customizado for usado no aplicativo, precisamos registrá-lo no ambiente de teste também antes de testar o aplicativo usando a classe IdlingRegistry conforme abaixo,

IdlingRegistry.getInstance().register(MyIdlingResource.getIdlingResource());

Além disso, ele pode ser removido assim que o teste for concluído conforme abaixo -

IdlingRegistry.getInstance().unregister(MyIdlingResource.getIdlingResource());

O Espresso fornece essa funcionalidade em um pacote separado, e o pacote precisa ser configurado conforme abaixo no app.gradle.

dependencies {
   implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
   androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1"
}

Aplicativo de amostra

Vamos criar um aplicativo simples para listar as frutas obtendo-as de um serviço da web em um thread separado e, em seguida, testá-lo usando o conceito de recurso inativo.

  • Inicie o Android Studio.

  • Crie um novo projeto conforme discutido anteriormente e nomeie-o, MyIdlingFruitApp

  • Migre o aplicativo para a estrutura AndroidX usando Refactor → Migrar para o menu de opções AndroidX .

  • Adicione a biblioteca de recursos de inatividade do espresso no app / build.gradle (e sincronize-o) conforme especificado abaixo,

dependencies {
   implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
   androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1"
}
  • Remova o design padrão na atividade principal e adicione ListView. O conteúdo do activity_main.xml é o seguinte,

<?xml version = "1.0" encoding = "utf-8"?>
<RelativeLayout xmlns:android = "http://schemas.android.com/apk/res/android"
   xmlns:app = "http://schemas.android.com/apk/res-auto"
   xmlns:tools = "http://schemas.android.com/tools"
   android:layout_width = "match_parent"
   android:layout_height = "match_parent"
   tools:context = ".MainActivity">
   <ListView
      android:id = "@+id/listView"
      android:layout_width = "wrap_content"
      android:layout_height = "wrap_content" />
</RelativeLayout>
  • Adicione um novo recurso de layout, item.xml para especificar o modelo de item da exibição de lista. O conteúdo do item.xml é o seguinte,

<?xml version = "1.0" encoding = "utf-8"?>
<TextView xmlns:android = "http://schemas.android.com/apk/res/android"
   android:id = "@+id/name"
   android:layout_width = "fill_parent"
   android:layout_height = "fill_parent"
   android:padding = "8dp"
/>
  • Crie uma nova classe - MyIdlingResource . MyIdlingResource é usado para manter nosso IdlingResource em um lugar e buscá-lo sempre que necessário. Vamos usar CountingIdlingResource em nosso exemplo.

package com.tutorialspoint.espressosamples.myidlingfruitapp;
import androidx.test.espresso.IdlingResource;
import androidx.test.espresso.idling.CountingIdlingResource;

public class MyIdlingResource {
   private static CountingIdlingResource mCountingIdlingResource =
      new CountingIdlingResource("my_idling_resource");
   public static void increment() {
      mCountingIdlingResource.increment();
   }
   public static void decrement() {
      mCountingIdlingResource.decrement();
   }
   public static IdlingResource getIdlingResource() {
      return mCountingIdlingResource;
   }
}
  • Declare uma variável global, mIdlingResource do tipo CountingIdlingResource na classe MainActivity conforme abaixo,

@Nullable
private CountingIdlingResource mIdlingResource = null;
  • Escreva um método privado para buscar lista de frutas da web como abaixo,

private ArrayList<String> getFruitList(String data) {
   ArrayList<String> fruits = new ArrayList<String>();
   try {
      // Get url from async task and set it into a local variable
      URL url = new URL(data);
      Log.e("URL", url.toString());
      
      // Create new HTTP connection
      HttpURLConnection conn = (HttpURLConnection) url.openConnection();
      
      // Set HTTP connection method as "Get"
      conn.setRequestMethod("GET");
      
      // Do a http request and get the response code
      int responseCode = conn.getResponseCode();
      
      // check the response code and if success, get response content
      if (responseCode == HttpURLConnection.HTTP_OK) {
         BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
         String line;
         StringBuffer response = new StringBuffer();
         while ((line = in.readLine()) != null) {
            response.append(line);
         }
         in.close();
         JSONArray jsonArray = new JSONArray(response.toString());
         Log.e("HTTPResponse", response.toString());
         for(int i = 0; i < jsonArray.length(); i++) {
            JSONObject jsonObject = jsonArray.getJSONObject(i);
            String name = String.valueOf(jsonObject.getString("name"));
            fruits.add(name);
         }
      } else {
         throw new IOException("Unable to fetch data from url");
      }
      conn.disconnect();
   } catch (IOException | JSONException e) {
      e.printStackTrace();
   }
   return fruits;
}
  • Crie uma nova tarefa no método onCreate () para buscar os dados da web usando nosso método getFruitList seguido pela criação de um novo adaptador e configurando-o para exibição de lista. Além disso, diminua o recurso inativo assim que nosso trabalho for concluído no thread. O código é o seguinte,

// Get data
class FruitTask implements Runnable {
   ListView listView;
   CountingIdlingResource idlingResource;
   FruitTask(CountingIdlingResource idlingRes, ListView listView) {
      this.listView = listView;
      this.idlingResource = idlingRes;
   }
   public void run() {
      //code to do the HTTP request
      final ArrayList<String> fruitList = getFruitList("http://<your domain or IP>/fruits.json");
      try {
         synchronized (this){
            runOnUiThread(new Runnable() {
               @Override
               public void run() {
                  // Create adapter and set it to list view
                  final ArrayAdapter adapter = new
                     ArrayAdapter(MainActivity.this, R.layout.item, fruitList);
                  ListView listView = (ListView)findViewById(R.id.listView);
                  listView.setAdapter(adapter);
               }
            });
         }
      } catch (Exception e) {
         e.printStackTrace();
      }
      if (!MyIdlingResource.getIdlingResource().isIdleNow()) {
         MyIdlingResource.decrement(); // Set app as idle.
      }
   }
}

Aqui, o URL da fruta é considerado http: // <seu domínio ou IP / fruits.json e é formatado como JSON. O conteúdo é o seguinte,

[ 
   {
      "name":"Apple"
   },
   {
      "name":"Banana"
   },
   {
      "name":"Cherry"
   },
   {
      "name":"Dates"
   },
   {
      "name":"Elderberry"
   },
   {
      "name":"Fig"
   },
   {
      "name":"Grapes"
   },
   {
      "name":"Grapefruit"
   },
   {
      "name":"Guava"
   },
   {
      "name":"Jack fruit"
   },
   {
      "name":"Lemon"
   },
   {
      "name":"Mango"
   },
   {
      "name":"Orange"
   },
   {
      "name":"Papaya"
   },
   {
      "name":"Pears"
   },
   {
      "name":"Peaches"
   },
   {
      "name":"Pineapple"
   },
   {
      "name":"Plums"
   },
   {
      "name":"Raspberry"
   },
   {
      "name":"Strawberry"
   },
   {
      "name":"Watermelon"
   }
]

Note - Coloque o arquivo em seu servidor web local e use-o.

  • Agora, encontre a visualização, crie um novo thread passando FruitTask , incremente o recurso inativo e, finalmente, inicie a tarefa.

// Find list view
ListView listView = (ListView) findViewById(R.id.listView);
Thread fruitTask = new Thread(new FruitTask(this.mIdlingResource, listView));
MyIdlingResource.increment();
fruitTask.start();
  • O código completo de MainActivity é o seguinte,

package com.tutorialspoint.espressosamples.myidlingfruitapp;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AppCompatActivity;
import androidx.test.espresso.idling.CountingIdlingResource;

import android.os.Bundle;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.ListView;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {
   @Nullable
   private CountingIdlingResource mIdlingResource = null;
   @Override
   protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      
      // Get data
      class FruitTask implements Runnable {
         ListView listView;
         CountingIdlingResource idlingResource;
         FruitTask(CountingIdlingResource idlingRes, ListView listView) {
            this.listView = listView;
            this.idlingResource = idlingRes;
         }
         public void run() {
            //code to do the HTTP request
            final ArrayList<String> fruitList = getFruitList(
               "http://<yourdomain or IP>/fruits.json");
            try {
               synchronized (this){
                  runOnUiThread(new Runnable() {
                     @Override
                     public void run() {
                        // Create adapter and set it to list view
                        final ArrayAdapter adapter = new ArrayAdapter(
                           MainActivity.this, R.layout.item, fruitList);
                        ListView listView = (ListView) findViewById(R.id.listView);
                        listView.setAdapter(adapter);
                     }
                  });
               }
            } catch (Exception e) {
               e.printStackTrace();
            }
            if (!MyIdlingResource.getIdlingResource().isIdleNow()) {
               MyIdlingResource.decrement(); // Set app as idle.
            }
         }
      }
      // Find list view
      ListView listView = (ListView) findViewById(R.id.listView);
      Thread fruitTask = new Thread(new FruitTask(this.mIdlingResource, listView));
      MyIdlingResource.increment();
      fruitTask.start();
   }
   private ArrayList<String> getFruitList(String data) {
      ArrayList<String> fruits = new ArrayList<String>();
      try {
         // Get url from async task and set it into a local variable
         URL url = new URL(data);
         Log.e("URL", url.toString());
         
         // Create new HTTP connection
         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
         
         // Set HTTP connection method as "Get"
         conn.setRequestMethod("GET");
         
         // Do a http request and get the response code
         int responseCode = conn.getResponseCode();
         
         // check the response code and if success, get response content
         if (responseCode == HttpURLConnection.HTTP_OK) {
            BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String line;
            StringBuffer response = new StringBuffer();
            while ((line = in.readLine()) != null) {
               response.append(line);
            }
            in.close();
            JSONArray jsonArray = new JSONArray(response.toString());
            Log.e("HTTPResponse", response.toString());
            
            for(int i = 0; i < jsonArray.length(); i++) {
               JSONObject jsonObject = jsonArray.getJSONObject(i);
               String name = String.valueOf(jsonObject.getString("name"));
               fruits.add(name);
            }
         } else {
            throw new IOException("Unable to fetch data from url");
         }
         conn.disconnect();
      } catch (IOException | JSONException e) {
         e.printStackTrace();
      }
      return fruits;
   }
}
  • Agora, adicione a configuração abaixo no arquivo de manifesto do aplicativo, AndroidManifest.xml

<uses-permission android:name = "android.permission.INTERNET" />
  • Agora, compile o código acima e execute o aplicativo. A captura de tela do aplicativo My Idling Fruit é a seguinte,

  • Agora, abra o arquivo ExampleInstrumentedTest.java e adicione ActivityTestRule conforme especificado abaixo,

@Rule
public ActivityTestRule<MainActivity> mActivityRule = 
   new ActivityTestRule<MainActivity>(MainActivity.class);
Also, make sure the test configuration is done in app/build.gradle
dependencies {
   testImplementation 'junit:junit:4.12'
   androidTestImplementation 'androidx.test:runner:1.1.1'
   androidTestImplementation 'androidx.test:rules:1.1.1'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
   implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
   androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1"
}
  • Adicione um novo caso de teste para testar a exibição de lista conforme abaixo,

@Before
public void registerIdlingResource() {
   IdlingRegistry.getInstance().register(MyIdlingResource.getIdlingResource());
}
@Test
public void contentTest() {
   // click a child item
   onData(allOf())
   .inAdapterView(withId(R.id.listView))
   .atPosition(10)
   .perform(click());
}
@After
public void unregisterIdlingResource() {
   IdlingRegistry.getInstance().unregister(MyIdlingResource.getIdlingResource());
}
  • Por fim, execute o caso de teste usando o menu de contexto do Android Studio e verifique se todos os casos de teste estão funcionando.