Espresso-testing-asynchronous-operations
非同期操作
この章では、Espresso Idling Resourcesを使用して非同期操作をテストする方法を学習します。
最新のアプリケーションの課題の1つは、スムーズなユーザーエクスペリエンスを提供することです。 スムーズなユーザーエクスペリエンスを提供するには、バックグラウンドで多くの作業を行い、アプリケーションプロセスが数ミリ秒を超えないようにします。 バックグラウンドタスクの範囲は、単純なものから、リモートAPI/データベースからデータを取得する高価で複雑なタスクまであります。 過去の課題に直面するために、開発者は、バックグラウンドスレッドでコストのかかる実行時間の長いタスクを記述し、バックグラウンドスレッドが完了するとメインの_UIThread_と同期していました。
マルチスレッドアプリケーションの開発が複雑な場合、テストケースの作成はさらに複雑になります。 たとえば、データベースから必要なデータをロードする前に、_AdapterView_をテストしないでください。 データのフェッチが別のスレッドで実行される場合、テストはスレッドが完了するまで待機する必要があります。 そのため、テスト環境はバックグラウンドスレッドとUIスレッド間で同期する必要があります。 Espressoは、マルチスレッドアプリケーションのテストに優れたサポートを提供します。 アプリケーションは次の方法でスレッドを使用し、エスプレッソはすべてのシナリオをサポートします。
ユーザーインターフェイスのスレッド化
Android SDKによって内部的に使用され、複雑なUI要素でスムーズなユーザーエクスペリエンスを提供します。 Espressoはこのシナリオを透過的にサポートし、構成や特別なコーディングは必要ありません。
非同期タスク
最新のプログラミング言語は、非同期プログラミングをサポートしており、スレッドプログラミングの複雑さを伴わずに軽量のスレッドを実行します。 非同期タスクは、espressoフレームワークによって透過的にサポートされます。
ユーザースレッド
開発者は、データベースから複雑なデータや大きなデータを取得するために新しいスレッドを開始する場合があります。 このシナリオをサポートするために、espressoはアイドリングリソースの概念を提供します。
この章では、アイドリングリソースの概念とその使用方法を学びましょう。
概要
アイドリングリソースの概念は非常にシンプルで直感的です。 基本的な考え方は、長時間実行されるプロセスが別のスレッドで開始されるたびに変数(ブール値)を作成して、プロセスが実行されているかどうかを識別し、テスト環境に登録することです。 テスト中に、テストランナーは登録された変数があればそれをチェックし、実行ステータスを見つけます。 実行ステータスがtrueの場合、テストランナーはステータスがfalseになるまで待機します。
Espressoは、実行ステータスを維持するためにIdlingResourcesインターフェイスを提供します。 実装する主なメソッドはisIdleNow()です。 isIdleNow()がtrueを返す場合、espressoはテストプロセスを再開するか、isIdleNow()がfalseを返すまで待機します。 IdlingResourcesを実装し、派生クラスを使用する必要があります。 Espressoは、組み込みのIdlingResources実装の一部も提供して、作業負荷を軽減します。 それらは次のとおりです。
CountingIdlingResource
これにより、実行中のタスクの内部カウンターが維持されます。 _increment()_および_decrement()_メソッドを公開します。 _increment()_はカウンターに1を追加し、_decrement()_はカウンターから1を削除します。 _isIdleNow()_は、アクティブなタスクがない場合にのみtrueを返します。
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"
}
サンプルアプリケーション
別のスレッドでWebサービスから取得して果物を一覧表示する簡単なアプリケーションを作成し、アイドルリソースの概念を使用してテストします。
- Androidスタジオを起動します。
- 前述のように新しいプロジェクトを作成し、MyIdlingFruitAppという名前を付けます
- Refactor→_AndroidX_への移行オプションメニューを使用して、アプリケーションをAndroidXフレームワークに_移行_します。
- 以下に指定されているように、_app/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を1か所に保持し、必要なときにいつでも取得するために使用されます。 この例では、_CountingIdlingResource_を使用します。
package com.finddevguides.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;
- 以下のように、Webからフルーツリストを取得するプライベートメソッドを記述します。
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;
}
- _getFruitList_メソッドを使用して_onCreate()_メソッドで新しいタスクを作成し、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<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"
}
]
注-ファイルをローカル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.finddevguides.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 App_のスクリーンショットは次のとおりです。
- 次に、_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のコンテキストメニューを使用してテストケースを実行し、すべてのテストケースが成功するかどうかを確認します。