Operazioni asincrone
In questo capitolo impareremo come testare le operazioni asincrone utilizzando Espresso Idling Resources.
Una delle sfide dell'applicazione moderna è fornire un'esperienza utente fluida. Fornire un'esperienza utente fluida richiede molto lavoro in background per assicurarsi che il processo di candidatura non richieda più di pochi millisecondi. L'attività in background varia da un'attività semplice a un'attività costosa e complessa di recupero dei dati da API / database remoti. Per affrontare la sfida in passato, uno sviluppatore era solito scrivere attività costose e di lunga esecuzione in un thread in background e sincronizzarsi con l' UIThread principale una volta completato il thread in background.
Se lo sviluppo di un'applicazione multi-thread è complesso, la scrittura di casi di test per essa è ancora più complessa. Ad esempio, non dovremmo testare un AdapterView prima che i dati necessari vengano caricati dal database. Se il recupero dei dati viene eseguito in un thread separato, il test deve attendere fino al completamento del thread. Quindi, l'ambiente di test dovrebbe essere sincronizzato tra thread in background e thread dell'interfaccia utente. Espresso fornisce un eccellente supporto per testare l'applicazione multi-thread. Un'applicazione utilizza il thread nei seguenti modi e espresso supporta ogni scenario.
Threading dell'interfaccia utente
Viene utilizzato internamente da Android SDK per fornire un'esperienza utente fluida con elementi dell'interfaccia utente complessi. Espresso supporta questo scenario in modo trasparente e non necessita di alcuna configurazione e codifica speciale.
Attività asincrona
I linguaggi di programmazione moderni supportano la programmazione asincrona per eseguire thread leggeri senza la complessità della programmazione dei thread. Anche l'attività asincrona è supportata in modo trasparente dal framework espresso.
Thread utente
Uno sviluppatore può avviare un nuovo thread per recuperare dati complessi o di grandi dimensioni dal database. Per supportare questo scenario, espresso fornisce il concetto di risorsa inattiva.
Consentitemi di imparare il concetto di risorsa inattiva e come utilizzarlo in questo capitolo.
Panoramica
Il concetto di risorsa inattiva è molto semplice e intuitivo. L'idea di base è creare una variabile (valore booleano) ogni volta che un processo a lunga esecuzione viene avviato in un thread separato per identificare se il processo è in esecuzione o meno e registrarlo nell'ambiente di test. Durante il test, il test runner controllerà la variabile registrata, se presente, e quindi ne troverà lo stato di esecuzione. Se lo stato di esecuzione è vero, il test runner attenderà finché lo stato non diventa falso.
Espresso fornisce un'interfaccia, IdlingResources allo scopo di mantenere lo stato di esecuzione. Il metodo principale da implementare è isIdleNow (). Se isIdleNow () restituisce true, espresso riprenderà il processo di test oppure aspetterà fino a quando isIdleNow () restituirà false. Dobbiamo implementare IdlingResources e utilizzare la classe derivata. Espresso fornisce anche alcune delle implementazioni IdlingResources integrate per facilitare il nostro carico di lavoro. Sono i seguenti,
CountingIdlingResource
Ciò mantiene un contatore interno dell'attività in esecuzione. Espone i metodi increment () e decrement () . increment () aggiunge uno al contatore e decrement () ne rimuove uno dal contatore. isIdleNow () restituisce true solo quando nessuna attività è attiva.
UriIdlingResource
È simile a CounintIdlingResource tranne per il fatto che il contatore deve essere zero per un periodo prolungato per accettare anche la latenza di rete.
IdlingThreadPoolExecutor
Questa è un'implementazione personalizzata di ThreadPoolExecutor per mantenere il numero di attività in esecuzione attive nel pool di thread corrente.
IdlingScheduledThreadPoolExecutor
È simile a IdlingThreadPoolExecutor , ma pianifica anche un'attività e un'implementazione personalizzata di ScheduledThreadPoolExecutor.
Se nell'applicazione viene utilizzata una qualsiasi delle implementazioni precedenti di IdlingResources o personalizzata, è necessario registrarla nell'ambiente di test prima di testare l'applicazione utilizzando la classe IdlingRegistry come di seguito
IdlingRegistry.getInstance().register(MyIdlingResource.getIdlingResource());
Inoltre, può essere rimosso una volta completato il test come di seguito:
IdlingRegistry.getInstance().unregister(MyIdlingResource.getIdlingResource());
Espresso fornisce questa funzionalità in un pacchetto separato e il pacchetto deve essere configurato come di seguito in app.gradle.
dependencies {
implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1"
}
Applicazione di esempio
Creiamo una semplice applicazione per elencare i frutti recuperandola da un servizio Web in un thread separato e quindi testarla utilizzando il concetto di risorsa inattiva.
Avvia Android Studio.
Crea un nuovo progetto come discusso in precedenza e chiamalo, MyIdlingFruitApp
Migrare l'applicazione al framework AndroidX utilizzando Refactor → Migrate to AndroidX option menu.
Aggiungi la libreria delle risorse di inattività espresso nell'app / build.gradle (e sincronizzala) come specificato di seguito,
dependencies {
implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1"
}
Rimuovi il design predefinito nell'attività principale e aggiungi ListView. Il contenuto di activity_main.xml è il seguente,
<?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>
Aggiungi nuova risorsa di layout, item.xml per specificare il modello di elemento della visualizzazione elenco. Il contenuto di item.xml è il seguente,
<?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"
/>
Crea una nuova classe: MyIdlingResource . MyIdlingResource viene utilizzato per tenere la nostra IdlingResource in un unico posto e recuperarla quando necessario. Useremo CountingIdlingResource nel nostro esempio.
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;
}
}
Dichiarare una variabile globale, mIdlingResource di tipo CountingIdlingResource nella classe MainActivity come di seguito,
@Nullable
private CountingIdlingResource mIdlingResource = null;
Scrivi un metodo privato per recuperare la lista della frutta dal web come di seguito,
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;
}
Crea una nuova attività nel metodo onCreate () per recuperare i dati dal web utilizzando il nostro metodo getFruitList seguito dalla creazione di un nuovo adattatore e impostandolo sulla visualizzazione elenco. Inoltre, decrementa la risorsa inattiva una volta che il nostro lavoro è stato completato nel thread. Il codice è il seguente,
// 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.
}
}
}
Qui, l'URL della frutta è considerato come http: // <il tuo dominio o IP / fruits.json ed è formattato come JSON. Il contenuto è il seguente,
[
{
"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 - Posiziona il file nel tuo server web locale e usalo.
Ora, trova la vista, crea un nuovo thread passando FruitTask , incrementa la risorsa inattiva e infine avvia l'attività.
// Find list view
ListView listView = (ListView) findViewById(R.id.listView);
Thread fruitTask = new Thread(new FruitTask(this.mIdlingResource, listView));
MyIdlingResource.increment();
fruitTask.start();
Il codice completo di MainActivity è il seguente,
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;
}
}
Ora aggiungi la configurazione di seguito nel file manifest dell'applicazione, AndroidManifest.xml
<uses-permission android:name = "android.permission.INTERNET" />
Ora, compila il codice sopra ed esegui l'applicazione. Lo screenshot dell'app My Idling Fruit è il seguente,
Ora, apri il file ExampleInstrumentedTest.java e aggiungi ActivityTestRule come specificato di seguito,
@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"
}
Aggiungi un nuovo scenario di test per testare la visualizzazione elenco come di seguito,
@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());
}
Infine, esegui il test case utilizzando il menu contestuale di Android Studio e controlla se tutti i test case hanno successo.