Hoạt động không đồng bộ
Trong chương này, chúng ta sẽ học cách kiểm tra các hoạt động không đồng bộ bằng cách sử dụng Espresso Idling Resources.
Một trong những thách thức của ứng dụng hiện đại là cung cấp trải nghiệm người dùng mượt mà. Cung cấp trải nghiệm người dùng mượt mà liên quan đến nhiều công việc trong nền để đảm bảo rằng quá trình ứng dụng không mất nhiều thời gian hơn vài mili giây. Nhiệm vụ nền có phạm vi từ nhiệm vụ đơn giản đến tốn kém và phức tạp là tìm nạp dữ liệu từ API / cơ sở dữ liệu từ xa. Để đối mặt với thách thức trước đây, một nhà phát triển đã từng viết tác vụ tốn kém và chạy lâu trong một chuỗi nền và đồng bộ hóa với UIThread chính sau khi chuỗi nền hoàn thành.
Nếu việc phát triển một ứng dụng đa luồng đã phức tạp thì việc viết các trường hợp kiểm thử cho nó còn phức tạp hơn. Ví dụ: chúng ta không nên kiểm tra một AdapterView trước khi dữ liệu cần thiết được tải từ cơ sở dữ liệu. Nếu việc tìm nạp dữ liệu được thực hiện trong một luồng riêng biệt, việc kiểm tra cần phải đợi cho đến khi luồng hoàn tất. Vì vậy, môi trường thử nghiệm nên được đồng bộ hóa giữa luồng nền và luồng giao diện người dùng. Espresso cung cấp một sự hỗ trợ tuyệt vời để thử nghiệm ứng dụng đa luồng. Một ứng dụng sử dụng chuỗi theo những cách sau và espresso hỗ trợ mọi tình huống.
Phân luồng giao diện người dùng
Nó được Android SDK sử dụng nội bộ để cung cấp trải nghiệm người dùng mượt mà với các phần tử giao diện người dùng phức tạp. Espresso hỗ trợ kịch bản này một cách minh bạch và không cần bất kỳ cấu hình và mã hóa đặc biệt nào.
Nhiệm vụ không đồng bộ
Các ngôn ngữ lập trình hiện đại hỗ trợ lập trình không đồng bộ để thực hiện phân luồng nhẹ mà không cần lập trình luồng phức tạp. Tác vụ async cũng được hỗ trợ một cách minh bạch bởi khuôn khổ espresso.
Chủ đề người dùng
Một nhà phát triển có thể bắt đầu một chuỗi mới để tìm nạp dữ liệu phức tạp hoặc lớn từ cơ sở dữ liệu. Để hỗ trợ tình huống này, espresso cung cấp khái niệm tài nguyên chạy không tải.
Hãy sử dụng tìm hiểu khái niệm tài nguyên không tải và cách sử dụng nó trong chương này.
Tổng quat
Khái niệm tài nguyên chạy không tải rất đơn giản và trực quan. Ý tưởng cơ bản là tạo một biến (giá trị boolean) bất cứ khi nào một quá trình chạy dài được bắt đầu trong một luồng riêng biệt để xác định xem quá trình có đang chạy hay không và đăng ký nó trong môi trường thử nghiệm. Trong quá trình thử nghiệm, người chạy thử nghiệm sẽ kiểm tra biến đã đăng ký, nếu có và sau đó tìm trạng thái đang chạy của nó. Nếu trạng thái đang chạy là đúng, người chạy thử nghiệm sẽ đợi cho đến khi trạng thái trở thành sai.
Espresso cung cấp một giao diện, IdlingResources nhằm mục đích duy trì trạng thái đang chạy. Phương thức chính để triển khai là isIdleNow (). Nếu isIdleNow () trả về true, espresso sẽ tiếp tục quá trình thử nghiệm hoặc nếu không, hãy đợi cho đến khi isIdleNow () trả về false. Chúng ta cần triển khai IdlingResources và sử dụng lớp dẫn xuất. Espresso cũng cung cấp một số triển khai IdlingResources tích hợp sẵn để giảm bớt khối lượng công việc của chúng tôi. Chúng như sau,
CountingIdlingResource
Điều này duy trì một bộ đếm bên trong của tác vụ đang chạy. Nó hiển thị các phương thức tăng () và giảm () . tăng () thêm một vào bộ đếm và giảm () xóa một khỏi bộ đếm. isIdleNow () chỉ trả về true khi không có tác vụ nào hoạt động.
UriIdlingResource
Điều này tương tự như CounintIdlingResource ngoại trừ việc bộ đếm cần bằng 0 trong thời gian kéo dài để tính cả độ trễ mạng.
IdlingThreadPoolExecutor
Đây là một triển khai tùy chỉnh của ThreadPoolExecutor để duy trì tác vụ chạy số đang hoạt động trong nhóm luồng hiện tại.
IdlingSchedisedThreadPoolExecutor
Điều này tương tự như IdlingThreadPoolExecutor , nhưng nó cũng lên lịch một tác vụ và triển khai tùy chỉnh của SchedisedThreadPoolExecutor .
Nếu bất kỳ triển khai nào ở trên của IdlingResources hoặc tùy chỉnh được sử dụng trong ứng dụng, chúng tôi cũng cần đăng ký nó vào môi trường thử nghiệm trước khi thử nghiệm ứng dụng bằng lớp IdlingRegistry như bên dưới,
IdlingRegistry.getInstance().register(MyIdlingResource.getIdlingResource());
Hơn nữa, nó có thể bị xóa sau khi hoàn tất quá trình kiểm tra như bên dưới
IdlingRegistry.getInstance().unregister(MyIdlingResource.getIdlingResource());
Espresso cung cấp chức năng này trong một gói riêng và gói này cần được định cấu hình như bên dưới trong app.gradle.
dependencies {
implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1"
}
Ứng dụng mẫu
Hãy để chúng tôi tạo một ứng dụng đơn giản để liệt kê các thành quả bằng cách tìm nạp nó từ một dịch vụ web trong một chuỗi riêng và sau đó, kiểm tra nó bằng cách sử dụng khái niệm tài nguyên không tải.
Khởi động Android studio.
Tạo dự án mới như đã thảo luận trước đó và đặt tên cho nó, MyIdlingFruitApp
Di chuyển ứng dụng sang khung AndroidX bằng Trình tái cấu trúc → Di chuyển sang menu tùy chọn AndroidX .
Thêm thư viện tài nguyên chạy không tải espresso trong app / build.gradle (và đồng bộ hóa nó) như được chỉ định bên dưới,
dependencies {
implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1"
}
Loại bỏ thiết kế mặc định trong hoạt động chính và thêm ListView. Nội dung của activity_main.xml như sau,
<?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>
Thêm tài nguyên bố cục mới, item.xml để chỉ định mẫu mục của dạng xem danh sách. Nội dung của item.xml như sau,
<?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"
/>
Tạo một lớp mới - MyIdlingResource . MyIdlingResource được sử dụng để giữ IdlingResource của chúng tôi ở một nơi và tìm nạp bất cứ khi nào cần thiết. Chúng tôi sẽ sử dụng CountingIdlingResource trong ví dụ của chúng tôi.
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;
}
}
Khai báo một biến toàn cục, mIdlingResource thuộc loại CountingIdlingResource trong lớp MainActivity như bên dưới,
@Nullable
private CountingIdlingResource mIdlingResource = null;
Viết một phương thức riêng để lấy danh sách trái cây từ web như bên dưới,
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;
}
Tạo một tác vụ mới trong phương thức onCreate () để tìm nạp dữ liệu từ web bằng phương thức getFruitList của chúng tôi, sau đó tạo một bộ điều hợp mới và đặt nó ở chế độ xem danh sách. Ngoài ra, giảm tài nguyên chạy không tải khi công việc của chúng ta hoàn thành trong luồng. Mã như sau,
// 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.
}
}
}
Ở đây, url trái cây được coi là http: // <miền của bạn hoặc IP / fruit.json và nó được định dạng là JSON. Nội dung như sau,
[
{
"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 - Đặt tệp vào máy chủ web cục bộ của bạn và sử dụng nó.
Bây giờ, tìm chế độ xem, tạo một luồng mới bằng cách chuyển FruitTask , tăng tài nguyên chạy không tải và cuối cùng bắt đầu tác vụ.
// Find list view
ListView listView = (ListView) findViewById(R.id.listView);
Thread fruitTask = new Thread(new FruitTask(this.mIdlingResource, listView));
MyIdlingResource.increment();
fruitTask.start();
Mã hoàn chỉnh của MainActivity như sau,
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;
}
}
Bây giờ, hãy thêm cấu hình bên dưới vào tệp kê khai ứng dụng, AndroidManifest.xml
<uses-permission android:name = "android.permission.INTERNET" />
Bây giờ, hãy biên dịch đoạn mã trên và chạy ứng dụng. Ảnh chụp màn hình của My Idling Fruit App như sau,
Bây giờ, hãy mở tệp ExampleIricalmentedTest.java và thêm ActivityTestRule như được chỉ định bên dưới,
@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"
}
Thêm một trường hợp thử nghiệm mới để kiểm tra chế độ xem danh sách như bên dưới,
@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());
}
Cuối cùng, chạy test case bằng menu ngữ cảnh của android studio và kiểm tra xem tất cả test case có thành công hay không.