In this chapter, we will learn how to test asynchronous operations using Espresso Idling Resources.
One of the challenges of the modern application is to provide smooth user experience. Providing smooth user experience involves lot of work in the background to make sure that the application process does not take longer than few milliseconds. Background task ranges from the simple one to costly and complex task of fetching data from remote API / database. To encounter the challenge in the past, a developer used to write costly and long running task in a background thread and sync up with the main UIThread once background thread is completed.
If developing a multi-threaded application is complex, then writing test cases for it is even more complex. For example, we should not test an AdapterView before the necessary data is loaded from the database. If fetching the data is done in a separate thread, the test needs to wait until the thread completes. So, the test environment should be synced between background thread and UI thread. Espresso provides an excellent support for testing the multi-threaded application. An application uses thread in the following ways and espresso supports every scenario.
It is internally used by android SDK to provide smooth user experience with complex UI elements. Espresso supports this scenario transparently and does not need any configuration and special coding.
Modern programming languages support async programming to do light weight threading without the complexity of thread programming. Async task is also supported transparently by espresso framework.
A developer may start a new thread to fetch complex or large data from database. To support this scenario, espresso provides idling resource concept.
Let use learn the concept of idling resource and how to to it in this chapter.
The concept of idling resource is very simple and intuitive. The basic idea is to create a variable (boolean value) whenever a long running process is started in a separate thread to identify whether the process is running or not and register it in the testing environment. During testing, the test runner will check the registered variable, if any found and then find its running status. If the running status is true, test runner will wait until the status become false.
Espresso provides an interface, IdlingResources for the purpose of maintaining the running status. The main method to implement is isIdleNow(). If isIdleNow() returns true, espresso will resume the testing process or else wait until isIdleNow() returns false. We need to implement IdlingResources and use the derived class. Espresso also provides some of the built-in IdlingResources implementation to ease our workload. They are as follows,
This maintains an internal counter of running task. It exposes increment() and decrement() methods. increment() adds one to the counter and decrement() removes one from the counter. isIdleNow() returns true only when no task is active.
This is similar to CounintIdlingResource except that the counter needs to be zero for extended period to take the network latency as well.
This is a custom implementation of ThreadPoolExecutor to maintain the number active running task in the current thread pool.
This is similar to IdlingThreadPoolExecutor, but it schedules a task as well and a custom implementation of ScheduledThreadPoolExecutor.
If any one of the above implementation of IdlingResources or a custom one is used in the application, we need to register it to the testing environment as well before testing the application using IdlingRegistry class as below,
IdlingRegistry.getInstance().register(MyIdlingResource.getIdlingResource());
Moreover, it can be removed once testing is completed as below −
IdlingRegistry.getInstance().unregister(MyIdlingResource.getIdlingResource());
Espresso provides this functionality in a separate package, and the package needs to be configured as below in the app.gradle.
dependencies { implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1' androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1" }
Let us create a simple application to list the fruits by fetching it from a web service in a separate thread and then, test it using idling resource concept.
Start Android studio.
Create new project as discussed earlier and name it, MyIdlingFruitApp
Migrate the application to AndroidX framework using Refactor → Migrate to AndroidX option menu.
Add espresso idling resource library in the app/build.gradle (and sync it) as specified below,
dependencies { implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1' androidTestImplementation "androidx.test.espresso.idling:idlingconcurrent:3.1.1" }
Remove the default design in the main activity and add ListView. The content of the activity_main.xml is as follows,
<?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>
Add new layout resource, item.xml to specify the item template of the list view. The content of the item.xml is as follows,
<?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" />
Create a new class – MyIdlingResource. MyIdlingResource is used to hold our IdlingResource in one place and fetch it whenever necessary. We are going to use CountingIdlingResource in our example.
package com.howcodex.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; } }
Declare a global variable, mIdlingResource of type CountingIdlingResource in the MainActivity class as below,
@Nullable private CountingIdlingResource mIdlingResource = null;
Write a private method to fetch fruit list from the web as below,
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; }
Create a new task in the onCreate() method to fetch the data from the web using our getFruitList method followed by the creation of a new adapter and setting it out to list view. Also, decrement the idling resource once our work is completed in the thread. The code is as follows,
// 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. } } }
Here, the fruit url is considered as http://<your domain or IP/fruits.json and it is formated as JSON. The content is as follows,
[ { "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 − Place the file in your local web server and use it.
Now, find the view, create a new thread by passing FruitTask, increment the idling resource and finally start the task.
// Find list view ListView listView = (ListView) findViewById(R.id.listView); Thread fruitTask = new Thread(new FruitTask(this.mIdlingResource, listView)); MyIdlingResource.increment(); fruitTask.start();
The complete code of MainActivity is as follows,
package com.howcodex.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; } }
Now, add below configuration in the application manifest file, AndroidManifest.xml
<uses-permission android:name = "android.permission.INTERNET" />
Now, compile the above code and run the application. The screenshot of the My Idling Fruit App is as follows,
Now, open the ExampleInstrumentedTest.java file and add ActivityTestRule as specified below,
@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" }
Add a new test case to test the list view as below,
@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()); }
Finally, run the test case using android studio’s context menu and check whether all test cases are succeeding.