การดำเนินการแบบอะซิงโครนัส
ในบทนี้เราจะเรียนรู้วิธีทดสอบการทำงานแบบอะซิงโครนัสโดยใช้ Espresso Idling Resources
ความท้าทายประการหนึ่งของแอปพลิเคชันสมัยใหม่คือการมอบประสบการณ์การใช้งานที่ราบรื่น การมอบประสบการณ์การใช้งานที่ราบรื่นนั้นเกี่ยวข้องกับการทำงานอยู่เบื้องหลังจำนวนมากเพื่อให้แน่ใจว่าขั้นตอนการสมัครใช้เวลาไม่เกินสองสามมิลลิวินาที งานเบื้องหลังมีตั้งแต่งานง่ายๆไปจนถึงงานที่มีราคาแพงและซับซ้อนในการดึงข้อมูลจาก API / ฐานข้อมูลระยะไกล ในการเผชิญหน้ากับความท้าทายในอดีตนักพัฒนาเคยเขียนงานที่ต้องเสียค่าใช้จ่ายและใช้งานมานานในเธรดพื้นหลังและซิงค์กับUIThreadหลักเมื่อเธรดพื้นหลังเสร็จสิ้น
หากการพัฒนาแอปพลิเคชันแบบมัลติเธรดมีความซับซ้อนการเขียนกรณีทดสอบก็ยิ่งซับซ้อนมากขึ้น ตัวอย่างเช่นเราไม่ควรทดสอบAdapterViewก่อนที่ข้อมูลที่จำเป็นจะถูกโหลดจากฐานข้อมูล หากดึงข้อมูลในเธรดแยกต่างหากการทดสอบจะต้องรอจนกว่าเธรดจะเสร็จสมบูรณ์ ดังนั้นสภาพแวดล้อมการทดสอบควรจะซิงค์ระหว่างเธรดพื้นหลังและเธรด UI เอสเปรสโซให้การสนับสนุนที่ดีเยี่ยมสำหรับการทดสอบแอพพลิเคชั่นมัลติเธรด แอปพลิเคชันใช้เธรดด้วยวิธีต่อไปนี้และเอสเปรสโซรองรับทุกสถานการณ์
เธรดอินเทอร์เฟซผู้ใช้
Android SDK ถูกใช้ภายในเพื่อมอบประสบการณ์การใช้งานที่ราบรื่นด้วยองค์ประกอบ UI ที่ซับซ้อน เอสเปรสโซรองรับสถานการณ์นี้อย่างโปร่งใสและไม่ต้องการการกำหนดค่าและการเข้ารหัสพิเศษใด ๆ
งาน Async
ภาษาโปรแกรมสมัยใหม่รองรับการเขียนโปรแกรมแบบ async เพื่อทำเธรดน้ำหนักเบาโดยไม่ต้องมีความซับซ้อนของการเขียนโปรแกรมเธรด งาน Async ได้รับการสนับสนุนอย่างโปร่งใสโดยกรอบเอสเปรสโซ
เธรดผู้ใช้
นักพัฒนาอาจเริ่มเธรดใหม่เพื่อดึงข้อมูลที่ซับซ้อนหรือมีขนาดใหญ่จากฐานข้อมูล เพื่อรองรับสถานการณ์นี้เอสเปรสโซให้แนวคิดทรัพยากรที่ไม่ได้ใช้งาน
ให้ใช้เรียนรู้แนวคิดของทรัพยากรที่ไม่ทำงานและวิธีการใช้งานในบทนี้
ภาพรวม
แนวคิดของทรัพยากรที่ไม่ทำงานนั้นเรียบง่ายและใช้งานง่าย แนวคิดพื้นฐานคือการสร้างตัวแปร (ค่าบูลีน) เมื่อใดก็ตามที่กระบวนการที่รันเป็นเวลานานเริ่มต้นในเธรดแยกต่างหากเพื่อระบุว่ากระบวนการทำงานอยู่หรือไม่และลงทะเบียนในสภาพแวดล้อมการทดสอบ ในระหว่างการทดสอบผู้ทดสอบจะตรวจสอบตัวแปรที่ลงทะเบียนหากพบแล้วจะพบสถานะการทำงาน หากสถานะการวิ่งเป็นจริงนักวิ่งทดสอบจะรอจนกว่าสถานะจะกลายเป็นเท็จ
Espresso มีอินเทอร์เฟซ IdlingResources สำหรับวัตถุประสงค์ในการรักษาสถานะการทำงาน วิธีการหลักในการดำเนินการคือ isIdleNow () ถ้า isIdleNow () คืนค่าเป็นจริงเอสเปรสโซจะดำเนินการทดสอบต่อหรือมิฉะนั้นจะรอจนกว่า isIdleNow () จะส่งคืนเท็จ เราจำเป็นต้องใช้ IdlingResources และใช้คลาสที่ได้รับ นอกจากนี้เอสเปรสโซยังมีการใช้งาน IdlingResources ในตัวเพื่อลดภาระงานของเรา มีดังนี้
CountingIdlingResource
สิ่งนี้รักษาตัวนับภายในของงานที่กำลังรันอยู่ มันเสี่ยงที่เพิ่มขึ้น ()และลดลง ()วิธีการ Increment ()เพิ่มหนึ่งในตัวนับและการลด ()ลบหนึ่งตัวจากตัวนับ isIdleNow ()ส่งคืนค่าจริงเฉพาะเมื่อไม่มีงานแอ็คทีฟ
UriIdlingResource
สิ่งนี้คล้ายกับCounintIdlingResourceยกเว้นว่าตัวนับจะต้องเป็นศูนย์เป็นระยะเวลานานเพื่อใช้เวลาแฝงของเครือข่ายด้วย
IdlingThreadPoolExecutor
นี่เป็นการใช้งานThreadPoolExecutorแบบกำหนดเองเพื่อรักษาจำนวนงานที่กำลังรันอยู่ในเธรดพูลปัจจุบัน
IdlingScheduledThreadPoolExecutor
สิ่งนี้คล้ายกับIdlingThreadPoolExecutorแต่จะกำหนดเวลางานด้วยและการใช้งาน ScheduledThreadPoolExecutor แบบกำหนดเอง
หากการใช้งานIdlingResourcesข้างต้นหรือการใช้งานที่กำหนดเองใด ๆ ในแอปพลิเคชันเราจำเป็นต้องลงทะเบียนเข้ากับสภาพแวดล้อมการทดสอบก่อนที่จะทดสอบแอปพลิเคชันโดยใช้คลาสIdlingRegistryดังต่อไปนี้
IdlingRegistry.getInstance().register(MyIdlingResource.getIdlingResource());
นอกจากนี้ยังสามารถลบออกได้เมื่อการทดสอบเสร็จสิ้นดังต่อไปนี้ -
IdlingRegistry.getInstance().unregister(MyIdlingResource.getIdlingResource());
Espresso มีฟังก์ชันนี้ในแพ็คเกจแยกต่างหากและต้องกำหนดค่าแพ็คเกจตามด้านล่างใน app.gradle
dependencies {
implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1"
}
แอปพลิเคชันตัวอย่าง
ให้เราสร้างแอปพลิเคชั่นง่ายๆเพื่อแสดงรายการผลไม้โดยดึงมาจากบริการเว็บในเธรดแยกจากนั้นทดสอบโดยใช้แนวคิดทรัพยากรที่ไม่ได้ใช้งาน
เริ่ม Android studio
สร้างโครงการใหม่ตามที่กล่าวไว้ก่อนหน้านี้และตั้งชื่อว่า MyIdlingFruitApp
ย้ายแอปพลิเคชันไปยังเฟรมเวิร์ก AndroidX โดยใช้Refactor →ย้ายไปที่เมนูตัวเลือกAndroidX
เพิ่มไลบรารีทรัพยากรที่ไม่ได้ใช้งานเอสเปรสโซในแอพ / build.gradle (และซิงค์) ตามที่ระบุด้านล่าง
dependencies {
implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1"
}
ลบการออกแบบเริ่มต้นในกิจกรรมหลักและเพิ่ม ListView เนื้อหาของactivity_main.xmlมีดังนี้
<?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>
เพิ่มทรัพยากรเค้าโครงใหม่item.xmlเพื่อระบุเทมเพลตรายการของมุมมองรายการ เนื้อหาของitem.xmlมีดังนี้
<?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"
/>
สร้างคลาสใหม่ - MyIdlingResource MyIdlingResourceใช้เพื่อเก็บ IdlingResource ของเราไว้ในที่เดียวและดึงข้อมูลเมื่อจำเป็น เราจะใช้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;
}
}
ประกาศตัวแปรส่วนกลางmIdlingResourceของชนิดCountingIdlingResourceในคลาสMainActivityดังต่อไปนี้
@Nullable
private CountingIdlingResource mIdlingResource = null;
เขียนวิธีการส่วนตัวเพื่อดึงรายชื่อผลไม้จากเว็บด้านล่าง
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;
}
สร้างงานใหม่ในเมธอด onCreate ()เพื่อดึงข้อมูลจากเว็บโดยใช้เมธอดgetFruitListของเราตามด้วยการสร้างอะแดปเตอร์ใหม่และตั้งค่าเป็นมุมมองรายการ นอกจากนี้ลดทรัพยากรที่ไม่ทำงานเมื่องานของเราเสร็จสมบูรณ์ในเธรด รหัสมีดังนี้
// 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.
}
}
}
ที่นี่ URL ผลไม้ถือเป็นhttp: // <โดเมนของคุณหรือ IP / fruits.jsonและอยู่ในรูปแบบ JSON เนื้อหามีดังนี้
[
{
"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 - วางไฟล์ไว้ในเว็บเซิร์ฟเวอร์ของคุณและใช้งานได้
ตอนนี้ค้นหามุมมองสร้างเธรดใหม่โดยส่งFruitTaskเพิ่มทรัพยากรที่ไม่ทำงานและในที่สุดก็เริ่มงาน
// Find list view
ListView listView = (ListView) findViewById(R.id.listView);
Thread fruitTask = new Thread(new FruitTask(this.mIdlingResource, listView));
MyIdlingResource.increment();
fruitTask.start();
รหัสที่สมบูรณ์ของMainActivityมีดังต่อไปนี้
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;
}
}
ตอนนี้เพิ่มการกำหนดค่าด้านล่างในไฟล์รายการแอปพลิเคชันAndroidManifest.xml
<uses-permission android:name = "android.permission.INTERNET" />
ตอนนี้รวบรวมรหัสด้านบนและเรียกใช้แอปพลิเคชัน ภาพหน้าจอของแอป My Idling Fruitมีดังนี้
ตอนนี้เปิดไฟล์ExampleInstrumentedTest.javaและเพิ่ม ActivityTestRule ตามที่ระบุด้านล่าง
@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"
}
เพิ่มกรณีทดสอบใหม่เพื่อทดสอบมุมมองรายการด้านล่าง
@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());
}
สุดท้ายเรียกใช้กรณีทดสอบโดยใช้เมนูบริบทของ android studio และตรวจสอบว่ากรณีทดสอบทั้งหมดประสบความสำเร็จหรือไม่