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.