Opérations asynchrones

Dans ce chapitre, nous allons apprendre à tester les opérations asynchrones à l'aide des ressources Espresso Idling.

L'un des défis de l'application moderne est de fournir une expérience utilisateur fluide. Fournir une expérience utilisateur fluide implique beaucoup de travail en arrière-plan pour s'assurer que le processus de candidature ne prend pas plus de quelques millisecondes. La tâche en arrière-plan va de la tâche simple à la tâche coûteuse et complexe de récupération des données à partir d'une API / base de données distante. Pour relever le défi dans le passé, un développeur avait l'habitude d'écrire une tâche coûteuse et longue dans un thread d'arrière-plan et de se synchroniser avec l' UIThread principal une fois le thread d'arrière-plan terminé.

Si le développement d'une application multi-thread est complexe, l'écriture de cas de test est encore plus complexe. Par exemple, nous ne devons pas tester un AdapterView avant que les données nécessaires ne soient chargées à partir de la base de données. Si l'extraction des données est effectuée dans un thread séparé, le test doit attendre la fin du thread. Ainsi, l'environnement de test doit être synchronisé entre le thread d'arrière-plan et le thread d'interface utilisateur. Espresso fournit un excellent support pour tester l'application multi-thread. Une application utilise le fil des manières suivantes et espresso prend en charge tous les scénarios.

Threading de l'interface utilisateur

Il est utilisé en interne par le SDK Android pour offrir une expérience utilisateur fluide avec des éléments d'interface utilisateur complexes. Espresso prend en charge ce scénario de manière transparente et ne nécessite aucune configuration ni codage spécial.

Tâche asynchrone

Les langages de programmation modernes prennent en charge la programmation asynchrone pour effectuer des threads légers sans la complexité de la programmation des threads. La tâche asynchrone est également prise en charge de manière transparente par le framework espresso.

Fil de discussion utilisateur

Un développeur peut démarrer un nouveau thread pour récupérer des données complexes ou volumineuses de la base de données. Pour prendre en charge ce scénario, l'espresso fournit un concept de ressource au ralenti.

Laissez l'utilisation apprendre le concept de ressource de marche au ralenti et comment l'utiliser dans ce chapitre.

Aperçu

Le concept de ressource de ralenti est très simple et intuitif. L'idée de base est de créer une variable (valeur booléenne) chaque fois qu'un processus de longue durée est démarré dans un thread séparé pour identifier si le processus est en cours d'exécution ou non et l'enregistrer dans l'environnement de test. Pendant le test, le testeur vérifiera la variable enregistrée, le cas échéant, puis trouvera son état en cours d'exécution. Si l'état d'exécution est vrai, le lanceur de test attendra que l'état devienne faux.

Espresso fournit une interface, IdlingResources, dans le but de maintenir l'état de fonctionnement. La principale méthode à implémenter est isIdleNow (). Si isIdleNow () retourne true, espresso reprendra le processus de test ou attendra que isIdleNow () retourne false. Nous devons implémenter IdlingResources et utiliser la classe dérivée. Espresso fournit également une partie de l'implémentation IdlingResources intégrée pour alléger notre charge de travail. Ils sont comme suit,

CountingIdlingResource

Cela maintient un compteur interne de la tâche en cours d'exécution. Il expose les méthodes increment () et decrement () . increment () en ajoute un au compteur et decrement () en supprime un du compteur. isIdleNow () renvoie true uniquement lorsqu'aucune tâche n'est active.

UriIdlingResource

Ceci est similaire à CounintIdlingResource, sauf que le compteur doit être à zéro pendant une période prolongée pour prendre également la latence du réseau.

IdlingThreadPoolExecutor

Il s'agit d'une implémentation personnalisée de ThreadPoolExecutor pour conserver le nombre de tâches en cours d'exécution actives dans le pool de threads actuel.

IdlingScheduledThreadPoolExecutor

Ceci est similaire à IdlingThreadPoolExecutor , mais il planifie également une tâche et une implémentation personnalisée de ScheduledThreadPoolExecutor.

Si l'une des implémentations d' IdlingResources ci-dessus ou une implémentation personnalisée est utilisée dans l'application, nous devons également l'enregistrer dans l'environnement de test avant de tester l'application à l'aide de la classe IdlingRegistry comme ci-dessous,

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

De plus, il peut être supprimé une fois le test terminé comme ci-dessous -

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

Espresso fournit cette fonctionnalité dans un package séparé, et le package doit être configuré comme ci-dessous dans app.gradle.

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

Exemple d'application

Créons une application simple pour lister les fruits en les récupérant à partir d'un service Web dans un thread séparé, puis testons-la en utilisant le concept de ressource inactive.

  • Démarrez le studio Android.

  • Créez un nouveau projet comme indiqué précédemment et nommez-le MyIdlingFruitApp

  • Migrez l'application vers le framework AndroidX à l'aide de Refactor → Migrer vers le menu d'options AndroidX .

  • Ajoutez la bibliothèque de ressources espresso idling dans l' application / build.gradle (et synchronisez-la) comme spécifié ci-dessous,

dependencies {
   implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
   androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1"
}
  • Supprimez la conception par défaut dans l'activité principale et ajoutez ListView. Le contenu de activity_main.xml est le suivant,

<?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>
  • Ajoutez une nouvelle ressource de mise en page, item.xml pour spécifier le modèle d'élément de la vue de liste. Le contenu du fichier item.xml est le suivant,

<?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"
/>
  • Créez une nouvelle classe - MyIdlingResource . MyIdlingResource est utilisé pour conserver notre IdlingResource en un seul endroit et le récupérer chaque fois que nécessaire. Nous allons utiliser CountingIdlingResource dans notre exemple.

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;
   }
}
  • Déclarez une variable globale, mIdlingResource de type CountingIdlingResource dans la classe MainActivity comme ci-dessous,

@Nullable
private CountingIdlingResource mIdlingResource = null;
  • Écrivez une méthode privée pour récupérer la liste des fruits sur le Web comme ci-dessous,

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;
}
  • Créez une nouvelle tâche dans la méthode onCreate () pour récupérer les données sur le Web à l'aide de notre méthode getFruitList , puis créez un nouvel adaptateur et définissez-le en vue liste. De plus, décrémentez la ressource inactive une fois notre travail terminé dans le thread. Le code est comme suit,

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

Ici, l'URL du fruit est considérée comme http: // <votre domaine ou IP / fruits.json et elle est formatée en JSON. Le contenu est le suivant,

[ 
   {
      "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 - Placez le fichier sur votre serveur Web local et utilisez-le.

  • Maintenant, trouvez la vue, créez un nouveau thread en passant FruitTask , incrémentez la ressource inactive et enfin lancez la tâche.

// Find list view
ListView listView = (ListView) findViewById(R.id.listView);
Thread fruitTask = new Thread(new FruitTask(this.mIdlingResource, listView));
MyIdlingResource.increment();
fruitTask.start();
  • Le code complet de MainActivity est le suivant,

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;
   }
}
  • Maintenant, ajoutez la configuration ci-dessous dans le fichier manifeste de l'application, AndroidManifest.xml

<uses-permission android:name = "android.permission.INTERNET" />
  • Maintenant, compilez le code ci-dessus et exécutez l'application. La capture d'écran de l'application My Idling Fruit est la suivante,

  • Maintenant, ouvrez le fichier ExampleInstrumentedTest.java et ajoutez ActivityTestRule comme spécifié ci-dessous,

@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"
}
  • Ajoutez un nouveau cas de test pour tester la vue de liste comme ci-dessous,

@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());
}
  • Enfin, exécutez le scénario de test à l'aide du menu contextuel d'Android Studio et vérifiez si tous les scénarios de test réussissent.