Асинхронные операции
В этой главе мы узнаем, как тестировать асинхронные операции с помощью ресурсов Espresso Idling.
Одна из задач современного приложения - обеспечить удобство работы пользователей. Обеспечение плавного взаимодействия с пользователем включает в себя много работы в фоновом режиме, чтобы гарантировать, что процесс приложения не займет больше нескольких миллисекунд. Фоновая задача варьируется от простой до дорогостоящей и сложной задачи получения данных из удаленного API / базы данных. Чтобы столкнуться с этой проблемой в прошлом, разработчик писал дорогостоящую и длительную задачу в фоновом потоке и синхронизировался с основным UIThread после завершения фонового потока.
Если разработка многопоточного приложения сложна, то написание тестовых примеров для него еще сложнее. Например, мы не должны тестировать AdapterView до того, как необходимые данные будут загружены из базы данных. Если выборка данных выполняется в отдельном потоке, тест должен дождаться завершения потока. Итак, тестовая среда должна быть синхронизирована между фоновым потоком и потоком пользовательского интерфейса. Espresso обеспечивает отличную поддержку для тестирования многопоточного приложения. Приложение использует поток следующими способами, и эспрессо поддерживает все сценарии.
Потоковая обработка пользовательского интерфейса
Он используется внутри Android SDK для обеспечения плавного взаимодействия со сложными элементами пользовательского интерфейса. Espresso прозрачно поддерживает этот сценарий и не требует какой-либо настройки и специального кодирования.
Асинхронная задача
Современные языки программирования поддерживают асинхронное программирование для облегчения потоковой передачи без сложного программирования потоков. Асинхронная задача также прозрачно поддерживается фреймворком espresso.
Пользовательский поток
Разработчик может запустить новый поток для извлечения сложных или больших данных из базы данных. Для поддержки этого сценария в эспрессо предусмотрена концепция ресурса холостого хода.
Давайте изучим понятие ресурса холостого хода и способы его использования в этой главе.
Обзор
Концепция ресурса холостого хода очень проста и интуитивно понятна. Основная идея состоит в том, чтобы создавать переменную (логическое значение) всякий раз, когда длительный процесс запускается в отдельном потоке, чтобы определить, выполняется ли процесс или нет, и зарегистрировать его в среде тестирования. Во время тестирования средство запуска тестов проверяет зарегистрированную переменную, если таковая обнаружена, а затем находит ее рабочий статус. Если статус выполнения - истина, средство выполнения тестов будет ждать, пока статус не станет ложным.
Espresso предоставляет интерфейс IdlingResources для поддержания текущего статуса. Основной метод реализации - isIdleNow (). Если isIdleNow () возвращает true, эспрессо возобновит процесс тестирования или дождется, пока isIdleNow () не вернет false. Нам нужно реализовать IdlingResources и использовать производный класс. Espresso также предоставляет некоторые встроенные реализации IdlingResources для облегчения нашей рабочей нагрузки. Они следующие:
CountingIdlingResource
Это поддерживает внутренний счетчик запущенной задачи. Он предоставляет методы инкремента () и декремента () . increment () добавляет единицу к счетчику, а Decment () удаляет единицу со счетчика. 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.
Создайте новый проект, как обсуждалось ранее, и назовите его MyIdlingFruitApp.
Перенести приложение в рамках 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 в одном месте и получения его при необходимости. Мы собираемся использовать в нашем примере 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 / fruit.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 и проверьте, все ли тестовые примеры выполнены успешно.