Operaciones asincrónicas

En este capítulo, aprenderemos cómo probar operaciones asincrónicas usando Espresso Idling Resources.

Uno de los desafíos de la aplicación moderna es proporcionar una experiencia de usuario fluida. Proporcionar una experiencia de usuario fluida implica mucho trabajo en segundo plano para asegurarse de que el proceso de solicitud no demore más de unos pocos milisegundos. La tarea en segundo plano va desde la simple hasta la costosa y compleja tarea de obtener datos de una API / base de datos remota. Para enfrentar el desafío en el pasado, un desarrollador solía escribir tareas costosas y de larga duración en un hilo de fondo y sincronizar con el UIThread principal una vez que se completa el hilo de fondo.

Si desarrollar una aplicación multiproceso es complejo, escribir casos de prueba para ella es aún más complejo. Por ejemplo, no deberíamos probar un AdapterView antes de que se carguen los datos necesarios desde la base de datos. Si la obtención de datos se realiza en un hilo separado, la prueba debe esperar hasta que se complete el hilo. Por lo tanto, el entorno de prueba debe sincronizarse entre el hilo de fondo y el hilo de la interfaz de usuario. Espresso proporciona un excelente soporte para probar la aplicación multiproceso. Una aplicación utiliza subprocesos de las siguientes formas y espresso admite todos los escenarios.

Subprocesos de la interfaz de usuario

El SDK de Android lo utiliza internamente para proporcionar una experiencia de usuario fluida con elementos de interfaz de usuario complejos. Espresso admite este escenario de forma transparente y no necesita ninguna configuración ni codificación especial.

Tarea asincrónica

Los lenguajes de programación modernos admiten la programación asíncrona para realizar subprocesos ligeros sin la complejidad de la programación de subprocesos. La tarea asíncrona también es compatible de forma transparente con el marco de espresso.

Hilo de usuario

Un desarrollador puede iniciar un nuevo hilo para obtener datos grandes o complejos de la base de datos. Para respaldar este escenario, espresso proporciona un concepto de recurso inactivo.

Aprendamos el concepto de recurso inactivo y cómo hacerlo en este capítulo.

Visión de conjunto

El concepto de recurso inactivo es muy simple e intuitivo. La idea básica es crear una variable (valor booleano) cada vez que se inicia un proceso de larga ejecución en un hilo separado para identificar si el proceso se está ejecutando o no y registrarlo en el entorno de prueba. Durante la prueba, el corredor de pruebas verificará la variable registrada, si encuentra alguna, y luego encontrará su estado de ejecución. Si el estado de ejecución es verdadero, el corredor de prueba esperará hasta que el estado sea falso.

Espresso proporciona una interfaz, IdlingResources, con el fin de mantener el estado de ejecución. El método principal para implementar es isIdleNow (). Si isIdleNow () devuelve verdadero, espresso reanudará el proceso de prueba o esperará hasta que isIdleNow () devuelva falso. Necesitamos implementar IdlingResources y usar la clase derivada. Espresso también proporciona algunas de las implementaciones integradas de IdlingResources para facilitar nuestra carga de trabajo. Son los siguientes,

CountingIdlingResource

Esto mantiene un contador interno de la tarea en ejecución. Expone los métodos increment () y decrement () . increment () agrega uno al contador y decrement () elimina uno del contador. isIdleNow () devuelve verdadero solo cuando no hay ninguna tarea activa.

UriIdlingResource

Esto es similar a CounintIdlingResource, excepto que el contador debe ser cero durante un período prolongado para tomar también la latencia de la red.

InactivoThreadPoolExecutor

Esta es una implementación personalizada de ThreadPoolExecutor para mantener el número de tareas activas en ejecución en el grupo de subprocesos actual.

InactivoScheduledThreadPoolExecutor

Esto es similar a IdlingThreadPoolExecutor , pero también programa una tarea y una implementación personalizada de ScheduledThreadPoolExecutor.

Si se usa alguna de las implementaciones anteriores de IdlingResources o una personalizada en la aplicación, también debemos registrarla en el entorno de prueba antes de probar la aplicación usando la clase IdlingRegistry como se muestra a continuación,

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

Además, se puede eliminar una vez que se completen las pruebas como se muestra a continuación:

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

Espresso proporciona esta funcionalidad en un paquete separado, y el paquete debe configurarse como se muestra a continuación en app.gradle.

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

Aplicación de muestra

Creemos una aplicación simple para enumerar las frutas obteniéndola de un servicio web en un hilo separado y luego probémosla usando el concepto de recurso inactivo.

  • Inicie Android Studio.

  • Cree un nuevo proyecto como se discutió anteriormente y asígnele el nombre MyIdlingFruitApp

  • Migrar la aplicación a AndroidX marco usando Refactor → Migrar a AndroidX menú de opciones.

  • Agregue la biblioteca de recursos inactiva de espresso en la aplicación / build.gradle (y sincronícela) como se especifica a continuación,

dependencies {
   implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
   androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1"
}
  • Elimine el diseño predeterminado en la actividad principal y agregue ListView. El contenido de activity_main.xml es el siguiente,

<?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>
  • Agregue un nuevo recurso de diseño, item.xml para especificar la plantilla de elementos de la vista de lista. El contenido del item.xml es el siguiente,

<?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"
/>
  • Cree una nueva clase: MyIdlingResource . MyIdlingResource se utiliza para mantener nuestro IdlingResource en un solo lugar y buscarlo cuando sea necesario. Vamos a utilizar CountingIdlingResource en nuestro ejemplo.

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 una variable global, mIdlingResource de tipo CountingIdlingResource en la clase MainActivity como se muestra a continuación,

@Nullable
private CountingIdlingResource mIdlingResource = null;
  • Escriba un método privado para obtener la lista de frutas de la web como se muestra a continuación,

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;
}
  • Cree una nueva tarea en el método onCreate () para recuperar los datos de la web utilizando nuestro método getFruitList seguido de la creación de un nuevo adaptador y configurándolo en la vista de lista. Además, disminuya el recurso inactivo una vez que nuestro trabajo se complete en el hilo. El código es el siguiente,

// 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.
      }
   }
}

Aquí, la URL de la fruta se considera http: // <su dominio o IP / fruits.json y está formateada como JSON. El contenido es el siguiente,

[ 
   {
      "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 el archivo en su servidor web local y utilícelo.

  • Ahora, busque la vista, cree un nuevo hilo pasando FruitTask , incremente el recurso inactivo y finalmente inicie la tarea.

// Find list view
ListView listView = (ListView) findViewById(R.id.listView);
Thread fruitTask = new Thread(new FruitTask(this.mIdlingResource, listView));
MyIdlingResource.increment();
fruitTask.start();
  • El código completo de MainActivity es el siguiente,

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;
   }
}
  • Ahora, agregue la siguiente configuración en el archivo de manifiesto de la aplicación, AndroidManifest.xml

<uses-permission android:name = "android.permission.INTERNET" />
  • Ahora, compile el código anterior y ejecute la aplicación. La captura de pantalla de la aplicación My Idling Fruit es la siguiente:

  • Ahora, abra el archivo ExampleInstrumentedTest.java y agregue ActivityTestRule como se especifica a continuación,

@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"
}
  • Agregue un nuevo caso de prueba para probar la vista de lista como se muestra a continuación,

@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());
}
  • Finalmente, ejecute el caso de prueba usando el menú contextual de Android Studio y verifique si todos los casos de prueba están teniendo éxito.