📅  最后修改于: 2020-12-06 09:38:47             🧑  作者: Mango
在本章中,我们将学习如何使用Espresso空闲资源测试异步操作。
现代应用程序的挑战之一是提供流畅的用户体验。提供流畅的用户体验需要在后台进行大量工作,以确保应用程序过程不会超过几毫秒。后台任务的范围很广,从简单的任务到昂贵的复杂任务都是从远程API /数据库获取数据。为了解决过去的挑战,开发人员习惯于在后台线程中编写昂贵且运行时间长的任务,并在后台线程完成后与主UIThread同步。
如果开发多线程应用程序很复杂,那么为其编写测试用例就更加复杂。例如,在从数据库加载必要的数据之前,我们不应该测试AdapterView 。如果获取数据是在单独的线程中完成的,则测试需要等待直到线程完成。因此,测试环境应在后台线程和UI线程之间同步。 Espresso为测试多线程应用程序提供了出色的支持。应用程序通过以下方式使用线程,espresso支持每种情况。
它由android SDK内部使用,以通过复杂的UI元素提供流畅的用户体验。 Espresso透明地支持此方案,不需要任何配置和特殊编码。
现代编程语言支持异步编程,以执行轻量级线程,而无需复杂的线程编程。 espresso框架也透明地支持异步任务。
开发人员可以启动新线程以从数据库中获取复杂或大型数据。为了支持这种情况,espresso提供了空闲资源概念。
在本章中,让我们学习闲置资源的概念以及如何使用它。
空闲资源的概念非常简单直观。基本思想是,只要在单独的线程中启动了长时间运行的进程,便会创建一个变量(布尔值),以识别该进程是否正在运行,并将其注册到测试环境中。在测试过程中,测试运行程序将检查已注册的变量(如果找到),然后找到其运行状态。如果运行状态为true,则测试运行程序将等待,直到状态变为false。
Espresso提供了一个IdlingResources接口,用于维持运行状态。实现的主要方法是isIdleNow()。如果isIdleNow()返回true,则意式浓缩咖啡将继续测试过程,否则请等到isIdleNow()返回false。我们需要实现IdlingResources并使用派生类。 Espresso还提供了一些内置的IdlingResources实现,以减轻我们的工作量。它们如下
这维护了运行任务的内部计数器。它公开了增量()和减量()方法。 increment()将一个值添加到计数器,而decrement()将一个值从计数器中删除。 isIdleNow()仅在没有活动的任务时返回true。
这与CounintIdlingResource相似,不同之处在于计数器在延长的时间段内需要为零才能获得网络延迟。
这是ThreadPoolExecutor的自定义实现,用于维护当前线程池中正在运行的编号任务。
这类似于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"
}
让我们创建一个简单的应用程序,通过在单独的线程中从Web服务获取水果来列出水果,然后使用空闲资源概念对其进行测试。
启动Android Studio。
如前所述创建新项目并将其命名为MyIdlingFruitApp
使用重构→迁移到AndroidX选项菜单将应用程序迁移到AndroidX框架。
如下所示在app / build.gradle中添加espresso空闲资源库(并对其进行同步),
dependencies {
implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1"
}
在主活动中删除默认设计,然后添加ListView。 activity_main.xml的内容如下,
添加新的布局资源item.xml以指定列表视图的项目模板。 item.xml的内容如下,
创建一个新类– 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;
}
}
在MainActivity类中声明一个全局变量CountingIdlingResource类型的mIdlingResource ,如下所示,
@Nullable
private CountingIdlingResource mIdlingResource = null;
编写一个私有方法来从网上获取水果清单,如下所示,
private ArrayList getFruitList(String data) {
ArrayList fruits = new ArrayList();
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方法从Web上获取数据,然后创建一个新适配器并将其设置为列表视图。另外,一旦线程中的工作完成,就减少空闲资源。代码如下,
// 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 fruitList = getFruitList("http:///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.
}
}
}
在这里,水果网址被视为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"
}
]
注意-将文件放在本地Web服务器中并使用。
现在,找到视图,通过传递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 fruitList = getFruitList(
"http:///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 getFruitList(String data) {
ArrayList fruits = new ArrayList();
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中添加以下配置
现在,编译上面的代码并运行该应用程序。 My Idling Fruit App的屏幕截图如下,
现在,打开ExampleInstrumentedTest.java文件,并按如下所示添加ActivityTestRule,
@Rule
public ActivityTestRule mActivityRule =
new ActivityTestRule(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的上下文菜单运行测试用例,并检查是否所有测试用例都成功。