Operasi Asinkron

Dalam bab ini, kita akan mempelajari cara menguji operasi asinkron menggunakan Espresso Idling Resources.

Salah satu tantangan aplikasi modern adalah memberikan pengalaman pengguna yang lancar. Memberikan pengalaman pengguna yang lancar melibatkan banyak pekerjaan di latar belakang untuk memastikan bahwa proses aplikasi tidak memakan waktu lebih dari beberapa milidetik. Tugas latar belakang berkisar dari yang sederhana hingga tugas yang mahal dan kompleks untuk mengambil data dari API / database jarak jauh. Untuk menghadapi tantangan di masa lalu, pengembang biasa menulis tugas yang mahal dan berjalan lama di utas latar belakang dan menyinkronkan dengan UIThread utama setelah utas latar belakang selesai.

Jika mengembangkan aplikasi multi-utas itu rumit, maka menulis kasus uji untuk itu bahkan lebih kompleks. Misalnya, kita tidak boleh menguji AdapterView sebelum data yang diperlukan dimuat dari database. Jika pengambilan data dilakukan di thread terpisah, pengujian harus menunggu hingga thread selesai. Jadi, lingkungan pengujian harus disinkronkan antara thread latar belakang dan thread UI. Espresso memberikan dukungan yang sangat baik untuk menguji aplikasi multi-threaded. Aplikasi menggunakan utas dengan cara berikut dan espresso mendukung setiap skenario.

Threading Antarmuka Pengguna

Ini digunakan secara internal oleh Android SDK untuk memberikan pengalaman pengguna yang lancar dengan elemen UI yang kompleks. Espresso mendukung skenario ini secara transparan dan tidak memerlukan konfigurasi apa pun dan pengkodean khusus.

Tugas asinkron

Bahasa pemrograman modern mendukung pemrograman async untuk melakukan threading ringan tanpa kerumitan pemrograman utas. Tugas Async juga didukung secara transparan oleh framework espresso.

Utas pengguna

Pengembang mungkin memulai utas baru untuk mengambil data yang kompleks atau besar dari database. Untuk mendukung skenario ini, espresso menyediakan konsep resource idling.

Mari gunakan mempelajari konsep sumber daya pemalasan dan bagaimana menggunakannya dalam bab ini.

Gambaran

Konsep sumber daya pemalasan sangat sederhana dan intuitif. Ide dasarnya adalah membuat variabel (nilai boolean) setiap kali proses yang berjalan lama dimulai di thread terpisah untuk mengidentifikasi apakah proses sedang berjalan atau tidak dan mendaftarkannya di lingkungan pengujian. Selama pengujian, runner pengujian akan memeriksa variabel terdaftar, jika ada yang ditemukan dan kemudian menemukan status berjalannya. Jika status berjalan benar, runner pengujian akan menunggu sampai status menjadi salah.

Espresso menyediakan antarmuka IdlingResources untuk tujuan mempertahankan status berjalan. Metode utama untuk diimplementasikan adalah isIdleNow (). Jika isIdleNow () mengembalikan nilai true, espresso akan melanjutkan proses pengujian atau menunggu sampai isIdleNow () mengembalikan false. Kita perlu mengimplementasikan IdlingResources dan menggunakan kelas turunan. Espresso juga menyediakan beberapa implementasi IdlingResources bawaan untuk meringankan beban kerja kami. Mereka adalah sebagai berikut,

CountingIdlingResource

Ini mempertahankan penghitung internal tugas yang sedang berjalan. Ini memperlihatkan metode increment () dan decrement () . increment () menambahkan satu ke penghitung dan decrement () menghapus satu dari penghitung. isIdleNow () mengembalikan nilai true hanya jika tidak ada tugas yang aktif.

UriIdlingResource

Ini mirip dengan CounintIdlingResource kecuali bahwa penghitung harus nol untuk periode yang diperpanjang untuk mengambil latensi jaringan juga.

IdlingThreadPoolExecutor

Ini adalah implementasi kustom ThreadPoolExecutor untuk mempertahankan jumlah tugas yang berjalan aktif di kumpulan thread saat ini.

IdlingScheduledThreadPoolExecutor

Ini mirip dengan IdlingThreadPoolExecutor , tetapi menjadwalkan tugas dan implementasi kustom ScheduledThreadPoolExecutor.

Jika salah satu dari implementasi IdlingResources atau kustom di atas digunakan dalam aplikasi, kita perlu mendaftarkannya ke lingkungan pengujian juga sebelum menguji aplikasi menggunakan kelas IdlingRegistry seperti di bawah ini,

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

Selain itu, dapat dihapus setelah pengujian selesai seperti di bawah ini -

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

Espresso menyediakan fungsionalitas ini dalam paket terpisah, dan paket tersebut perlu dikonfigurasi seperti di bawah ini di app.gradle.

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

Aplikasi Sampel

Mari kita buat aplikasi sederhana untuk membuat daftar buah dengan mengambilnya dari layanan web di utas terpisah dan kemudian, mengujinya menggunakan konsep sumber daya pemalasan.

  • Mulai studio Android.

  • Buat proyek baru seperti yang dibahas sebelumnya dan beri nama, MyIdlingFruitApp

  • Migrasikan aplikasi ke framework AndroidX menggunakan Refactor → Migrate to AndroidX option menu.

  • Tambahkan pustaka resource idling espresso di app / build.gradle (dan sinkronkan) seperti yang ditentukan di bawah ini,

dependencies {
   implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
   androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1"
}
  • Hapus desain default di aktivitas utama dan tambahkan ListView. Isi dari activity_main.xml adalah sebagai berikut,

<?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>
  • Tambahkan sumber daya layout baru, item.xml untuk menentukan template item tampilan daftar. Isi item.xml adalah sebagai berikut,

<?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"
/>
  • Buat kelas baru - MyIdlingResource . MyIdlingResource digunakan untuk menyimpan IdlingResource di satu tempat dan mengambilnya kapan pun diperlukan. Kami akan menggunakan CountingIdlingResource dalam contoh kami.

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;
   }
}
  • Deklarasikan variabel global, mIdlingResource tipe CountingIdlingResource di kelas MainActivity seperti di bawah ini,

@Nullable
private CountingIdlingResource mIdlingResource = null;
  • Tulis metode pribadi untuk mengambil daftar buah dari web seperti di bawah ini,

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;
}
  • Buat tugas baru dalam metode onCreate () untuk mengambil data dari web menggunakan metode getFruitList kami diikuti dengan pembuatan adaptor baru dan menyetelnya ke tampilan daftar. Juga, kurangi sumber daya yang tidak aktif setelah pekerjaan kita selesai di utas. Kodenya adalah sebagai berikut,

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

Di sini, url buah dianggap sebagai http: // <domain atau IP / fruits.json Anda dan diformat sebagai JSON. Isinya sebagai berikut,

[ 
   {
      "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 - Tempatkan file di server web lokal Anda dan gunakan.

  • Sekarang, temukan tampilan, buat utas baru dengan meneruskan FruitTask , tambahkan sumber daya yang tidak aktif dan akhirnya mulai tugas.

// Find list view
ListView listView = (ListView) findViewById(R.id.listView);
Thread fruitTask = new Thread(new FruitTask(this.mIdlingResource, listView));
MyIdlingResource.increment();
fruitTask.start();
  • Kode lengkap MainActivity adalah sebagai berikut,

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;
   }
}
  • Sekarang, tambahkan konfigurasi di bawah ini dalam file manifes aplikasi, AndroidManifest.xml

<uses-permission android:name = "android.permission.INTERNET" />
  • Sekarang, kompilasi kode di atas dan jalankan aplikasi. Tangkapan layar dari Aplikasi My Idling Fruit adalah sebagai berikut,

  • Sekarang, buka file ExampleInstrumentedTest.java dan tambahkan ActivityTestRule seperti yang ditentukan di bawah ini,

@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"
}
  • Tambahkan kasus uji baru untuk menguji tampilan daftar seperti di bawah ini,

@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());
}
  • Terakhir, jalankan kasus pengujian menggunakan menu konteks android studio dan periksa apakah semua kasus pengujian berhasil.