Operacje asynchroniczne

W tym rozdziale dowiemy się, jak testować operacje asynchroniczne przy użyciu zasobów Espresso Idling.

Jednym z wyzwań współczesnej aplikacji jest zapewnienie płynnej obsługi. Zapewnienie płynnej obsługi użytkownika wymaga dużo pracy w tle, aby proces aplikacji nie trwał dłużej niż kilka milisekund. Zadania w tle obejmują zarówno proste, jak i kosztowne i złożone zadania pobierania danych ze zdalnego interfejsu API / bazy danych. Aby sprostać wyzwaniu w przeszłości, programista pisał kosztowne i długotrwałe zadanie w wątku w tle i synchronizował się z głównym UIThread po zakończeniu wątku w tle.

Jeśli tworzenie aplikacji wielowątkowej jest skomplikowane, to pisanie dla niej przypadków testowych jest jeszcze bardziej złożone. Na przykład nie powinniśmy testować AdapterView przed załadowaniem niezbędnych danych z bazy danych. Jeśli pobieranie danych odbywa się w osobnym wątku, test musi poczekać, aż wątek się zakończy. Dlatego środowisko testowe powinno być zsynchronizowane między wątkiem w tle i wątkiem interfejsu użytkownika. Espresso zapewnia doskonałe wsparcie przy testowaniu aplikacji wielowątkowych. Aplikacja wykorzystuje wątek w następujący sposób, a espresso obsługuje każdy scenariusz.

Obsługa wątków w interfejsie użytkownika

Jest używany wewnętrznie przez Android SDK, aby zapewnić płynne wrażenia użytkownika ze złożonymi elementami interfejsu użytkownika. Espresso obsługuje ten scenariusz w sposób przejrzysty i nie wymaga żadnej konfiguracji ani specjalnego kodowania.

Zadanie asynchroniczne

Nowoczesne języki programowania obsługują programowanie asynchroniczne w celu wykonywania lekkich wątków bez złożoności programowania wątków. Zadanie Async jest również obsługiwane w sposób przezroczysty przez framework espresso.

Wątek użytkownika

Programista może rozpocząć nowy wątek, aby pobrać złożone lub duże dane z bazy danych. Aby wesprzeć ten scenariusz, espresso zapewnia koncepcję zasobów bezczynnych.

Skorzystajmy z tego rozdziału, aby poznać koncepcję bezczynnego zasobu i jak to zrobić.

Przegląd

Koncepcja bezczynnego zasobu jest bardzo prosta i intuicyjna. Podstawowym pomysłem jest utworzenie zmiennej (wartość logiczna) za każdym razem, gdy długo działający proces jest uruchamiany w oddzielnym wątku, aby zidentyfikować, czy proces działa, czy nie, i zarejestrować go w środowisku testowym. Podczas testowania moduł uruchamiający testy sprawdzi zarejestrowaną zmienną, jeśli została znaleziona, a następnie znajdzie jej stan działania. Jeśli stan działania to prawda, moduł uruchamiający testy będzie czekał, aż stan stanie się fałszywy.

Espresso zapewnia interfejs IdlingResources w celu utrzymania statusu działania. Główną metodą do zaimplementowania jest isIdleNow (). Jeśli isIdleNow () zwróci true, espresso wznowi proces testowania lub zaczeka, aż isIdleNow () zwróci false. Musimy zaimplementować IdlingResources i użyć klasy pochodnej. Espresso zapewnia również część wbudowanej implementacji IdlingResources, aby zmniejszyć obciążenie pracą. Są one następujące:

CountingIdlingResource

Utrzymuje to wewnętrzny licznik uruchomionych zadań. Udostępnia metody inkrementacji () i dekrementacji () . Increment () dodaje jeden do licznika, a decrement () usuwa jeden z licznika. isIdleNow () zwraca wartość true tylko wtedy, gdy żadne zadanie nie jest aktywne.

UriIdlingResource

Jest to podobne do CounintIdlingResource, z tą różnicą, że licznik musi być równy zero przez dłuższy czas, aby również uwzględnić opóźnienie w sieci.

IdlingThreadPoolExecutor

Jest to niestandardowa implementacja ThreadPoolExecutor w celu utrzymania liczby aktywnych uruchomionych zadań w bieżącej puli wątków.

IdlingScheduledThreadPoolExecutor

Jest to podobne do IdlingThreadPoolExecutor , ale planuje również zadanie i niestandardową implementację ScheduledThreadPoolExecutor.

Jeżeli w aplikacji jest zastosowana którakolwiek z powyższych implementacji IdlingResources lub niestandardowa, musimy zarejestrować ją również w środowisku testowym przed przetestowaniem aplikacji przy użyciu klasy IdlingRegistry jak poniżej,

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

Co więcej, można go usunąć po zakończeniu testów, jak poniżej -

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

Espresso zapewnia tę funkcjonalność w osobnym pakiecie, który należy skonfigurować jak poniżej w app.gradle.

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

Przykładowa aplikacja

Stwórzmy prostą aplikację do wyświetlania listy owoców, pobierając ją z usługi internetowej w osobnym wątku, a następnie przetestujmy ją przy użyciu koncepcji zasobów bezczynnych.

  • Uruchom studio Android.

  • Utwórz nowy projekt zgodnie z wcześniejszym opisem i nazwij go MyIdlingFruitApp

  • Przenieś aplikację do frameworka AndroidX za pomocą menu opcji Refactor → Migrate to AndroidX .

  • Dodaj bibliotekę zasobów bezczynności espresso w app / build.gradle (i zsynchronizuj ją), jak określono poniżej,

dependencies {
   implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
   androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1"
}
  • Usuń domyślny projekt w głównym działaniu i Dodaj ListView. Zawartość pliku activity_main.xml jest następująca:

<?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>
  • Dodaj nowy zasób układu, item.xml, aby określić szablon elementu widoku listy. Zawartość pliku item.xml jest następująca:

<?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"
/>
  • Utwórz nową klasę - MyIdlingResource . MyIdlingResource służy do przechowywania naszego IdlingResource w jednym miejscu i pobierania go w razie potrzeby. W naszym przykładzie użyjemy 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;
   }
}
  • Zadeklaruj zmienną globalną mIdlingResource typu CountingIdlingResource w klasie MainActivity, jak poniżej,

@Nullable
private CountingIdlingResource mIdlingResource = null;
  • Napisz prywatną metodę pobierania listy owoców z sieci, jak poniżej,

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;
}
  • Utwórz nowe zadanie w metodzie onCreate () , aby pobrać dane z sieci za pomocą naszej metody getFruitList , a następnie utworzyć nowy adapter i ustawić go w widoku listy. Ponadto zmniejszaj bezczynny zasób po zakończeniu pracy w wątku. Kod jest następujący:

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

Tutaj adres URL owoców jest traktowany jako http: // <twoja domena lub adres IP / owoce.json i jest sformatowany jako JSON. Treść jest następująca,

[ 
   {
      "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 - Umieść plik na lokalnym serwerze WWW i użyj go.

  • Teraz znajdź widok, utwórz nowy wątek, przekazując FruitTask , zwiększ zasób na biegu jałowym i na koniec uruchom zadanie.

// Find list view
ListView listView = (ListView) findViewById(R.id.listView);
Thread fruitTask = new Thread(new FruitTask(this.mIdlingResource, listView));
MyIdlingResource.increment();
fruitTask.start();
  • Pełny kod MainActivity wygląda następująco:

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;
   }
}
  • Teraz dodaj poniższą konfigurację w pliku manifestu aplikacji, AndroidManifest.xml

<uses-permission android:name = "android.permission.INTERNET" />
  • Teraz skompiluj powyższy kod i uruchom aplikację. Zrzut ekranu aplikacji My Idling Fruit wygląda następująco:

  • Teraz otwórz plik ExampleInstrumentedTest.java i dodaj ActivityTestRule, jak określono poniżej,

@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"
}
  • Dodaj nowy przypadek testowy, aby przetestować widok listy, jak poniżej,

@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());
}
  • Na koniec uruchom przypadek testowy za pomocą menu kontekstowego Android Studio i sprawdź, czy wszystkie przypadki testowe się powiodły.