Asynchrone Operationen

In diesem Kapitel erfahren Sie, wie Sie asynchrone Vorgänge mit Espresso Idling Resources testen.

Eine der Herausforderungen der modernen Anwendung besteht darin, eine reibungslose Benutzererfahrung zu bieten. Die Bereitstellung einer reibungslosen Benutzererfahrung erfordert viel Arbeit im Hintergrund, um sicherzustellen, dass der Bewerbungsprozess nicht länger als einige Millisekunden dauert. Die Hintergrundaufgabe reicht von der einfachen bis zur kostspieligen und komplexen Aufgabe, Daten von der Remote-API / Datenbank abzurufen. Um der Herausforderung in der Vergangenheit zu begegnen, hat ein Entwickler kostspielige und lang laufende Aufgaben in einen Hintergrundthread geschrieben und nach Abschluss des Hintergrundthreads mit dem Haupt- UIThread synchronisiert .

Wenn die Entwicklung einer Multithread-Anwendung komplex ist, ist das Schreiben von Testfällen noch komplexer. Beispielsweise sollten wir eine AdapterView nicht testen, bevor die erforderlichen Daten aus der Datenbank geladen wurden. Wenn das Abrufen der Daten in einem separaten Thread erfolgt, muss der Test warten, bis der Thread abgeschlossen ist. Daher sollte die Testumgebung zwischen Hintergrundthread und UI-Thread synchronisiert werden. Espresso bietet eine hervorragende Unterstützung beim Testen der Multithread-Anwendung. Eine Anwendung verwendet Thread auf folgende Weise und Espresso unterstützt jedes Szenario.

Threading der Benutzeroberfläche

Es wird intern vom Android SDK verwendet, um eine reibungslose Benutzererfahrung mit komplexen UI-Elementen zu gewährleisten. Espresso unterstützt dieses Szenario transparent und benötigt keine Konfiguration und spezielle Codierung.

Asynchrone Aufgabe

Moderne Programmiersprachen unterstützen die asynchrone Programmierung, um leichtes Threading ohne die Komplexität der Thread-Programmierung durchzuführen. Die asynchrone Aufgabe wird auch transparent vom Espresso-Framework unterstützt.

Benutzer-Thread

Ein Entwickler kann einen neuen Thread starten, um komplexe oder große Daten aus der Datenbank abzurufen. Um dieses Szenario zu unterstützen, bietet Espresso ein Konzept für Leerlaufressourcen.

In diesem Kapitel lernen Sie das Konzept des Leerlaufs von Ressourcen und dessen Vorgehensweise kennen.

Überblick

Das Konzept der Leerlaufressource ist sehr einfach und intuitiv. Die Grundidee besteht darin, eine Variable (boolescher Wert) zu erstellen, wenn ein Prozess mit langer Laufzeit in einem separaten Thread gestartet wird, um festzustellen, ob der Prozess ausgeführt wird oder nicht, und ihn in der Testumgebung zu registrieren. Während des Tests überprüft der Testläufer die registrierte Variable, falls vorhanden, und ermittelt dann ihren Betriebsstatus. Wenn der Laufstatus wahr ist, wartet der Testläufer, bis der Status falsch wird.

Espresso bietet eine Schnittstelle, IdlingResources, um den Betriebsstatus aufrechtzuerhalten. Die Hauptmethode zur Implementierung ist isIdleNow (). Wenn isIdleNow () true zurückgibt, setzt espresso den Testvorgang fort oder wartet, bis isIdleNow () false zurückgibt. Wir müssen IdlingResources implementieren und die abgeleitete Klasse verwenden. Espresso bietet auch einige der integrierten IdlingResources-Implementierungen, um unsere Arbeitsbelastung zu verringern. Sie sind wie folgt,

CountingIdlingResource

Dadurch wird ein interner Zähler für die laufende Aufgabe verwaltet. Es macht die Methoden increment () und decrement () verfügbar. increment () fügt dem Zähler einen hinzu und decrement () entfernt einen vom Zähler. isIdleNow () gibt nur dann true zurück, wenn keine Aufgabe aktiv ist.

UriIdlingResource

Dies ähnelt CounintIdlingResource, außer dass der Zähler für einen längeren Zeitraum Null sein muss, um auch die Netzwerklatenz zu berücksichtigen.

IdlingThreadPoolExecutor

Dies ist eine benutzerdefinierte Implementierung von ThreadPoolExecutor , um die Anzahl der aktiven laufenden Aufgaben im aktuellen Thread-Pool beizubehalten.

IdlingScheduledThreadPoolExecutor

Dies ähnelt IdlingThreadPoolExecutor , plant jedoch auch eine Aufgabe und eine benutzerdefinierte Implementierung von ScheduledThreadPoolExecutor.

Wenn eine der oben genannten oder eine benutzerdefinierte Implementierung von IdlingResources in der Anwendung verwendet wird, müssen wir sie ebenfalls in der Testumgebung registrieren, bevor wir die Anwendung mit der folgenden IdlingRegistry- Klasse testen können.

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

Darüber hinaus kann es nach Abschluss des Tests wie folgt entfernt werden:

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

Espresso bietet diese Funktionalität in einem separaten Paket, und das Paket muss wie folgt in der app.gradle konfiguriert werden.

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

Beispielanwendung

Lassen Sie uns eine einfache Anwendung erstellen, um die Früchte aufzulisten, indem wir sie von einem Webdienst in einem separaten Thread abrufen und sie dann mithilfe des Ressourcenkonzepts im Leerlauf testen.

  • Starten Sie Android Studio.

  • Erstellen Sie ein neues Projekt wie zuvor beschrieben und nennen Sie es MyIdlingFruitApp

  • Migrieren Sie die Anwendung über das Optionsmenü Refactor → Auf AndroidX migrieren auf das AndroidX-Framework .

  • Fügen Sie die Espresso-Ressourcenbibliothek im Leerlauf in die app / build.gradle ein (und synchronisieren Sie sie) wie unten angegeben.

dependencies {
   implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
   androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1"
}
  • Entfernen Sie das Standarddesign in der Hauptaktivität und fügen Sie ListView hinzu. Der Inhalt der Datei activity_main.xml lautet wie folgt:

<?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>
  • Fügen Sie die neue Layoutressource item.xml hinzu , um die Elementvorlage der Listenansicht anzugeben. Der Inhalt der item.xml lautet wie folgt:

<?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"
/>
  • Erstellen Sie eine neue Klasse - MyIdlingResource . MyIdlingResource wird verwendet, um unsere IdlingResource an einem Ort zu halten und bei Bedarf abzurufen. In unserem Beispiel verwenden wir CountingIdlingResource .

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;
   }
}
  • Deklarieren Sie eine globale Variable, mIdlingResource vom Typ CountingIdlingResource, in der MainActivity- Klasse wie folgt :

@Nullable
private CountingIdlingResource mIdlingResource = null;
  • Schreiben Sie eine private Methode, um die Obstliste wie folgt aus dem Internet abzurufen.

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;
}
  • Erstellen Sie eine neue Aufgabe in der onCreate () -Methode, um die Daten mit unserer getFruitList- Methode aus dem Web abzurufen. Anschließend erstellen Sie einen neuen Adapter und legen ihn für die Listenansicht fest . Verringern Sie außerdem die Leerlaufressource, sobald unsere Arbeit im Thread abgeschlossen ist. Der Code lautet wie folgt:

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

Hier wird die Frucht-URL als http: // <Ihre Domain oder IP / obst.json betrachtet und als JSON formatiert . Der Inhalt ist wie folgt:

[ 
   {
      "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 - Platzieren Sie die Datei auf Ihrem lokalen Webserver und verwenden Sie sie.

  • Suchen Sie nun die Ansicht, erstellen Sie einen neuen Thread, indem Sie FruitTask übergeben , erhöhen Sie die Leerlaufressource und starten Sie schließlich die Aufgabe.

// Find list view
ListView listView = (ListView) findViewById(R.id.listView);
Thread fruitTask = new Thread(new FruitTask(this.mIdlingResource, listView));
MyIdlingResource.increment();
fruitTask.start();
  • Der vollständige Code von MainActivity lautet wie folgt:

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;
   }
}
  • Fügen Sie nun die folgende Konfiguration in die Anwendungsmanifestdatei AndroidManifest.xml ein

<uses-permission android:name = "android.permission.INTERNET" />
  • Kompilieren Sie nun den obigen Code und führen Sie die Anwendung aus. Der Screenshot der My Idling Fruit App sieht wie folgt aus:

  • Öffnen Sie nun die Datei ExampleInstrumentedTest.java und fügen Sie ActivityTestRule wie unten angegeben hinzu.

@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"
}
  • Fügen Sie einen neuen Testfall hinzu, um die Listenansicht wie folgt zu testen:

@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());
}
  • Führen Sie den Testfall schließlich über das Kontextmenü von Android Studio aus und überprüfen Sie, ob alle Testfälle erfolgreich sind.