Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Deeper insight into Android Espresso library v1

F1sherKK
September 22, 2016

Deeper insight into Android Espresso library v1

Presentation by Kamil Krzyk from Android Tech Talks #14 - 21.09.2016, Azimo Office, Cracow.

F1sherKK

September 22, 2016
Tweet

More Decks by F1sherKK

Other Decks in Programming

Transcript

  1. Plan for today: 1. Introduction 2. Fast Espresso lib overview

    3. Basic Espresso components 4. Test process - how does it work 5. UI thread vs Test thread 6. What is “idle state”? 7. IdlingResource
  2. Espresso includes: • espresso-core Core and basic View matchers, actions,

    and assertions. • espresso-contrib Support for DatePicker, RecyclerView, Drawer and more.
  3. Espresso includes: • espresso-core Core and basic View matchers, actions,

    and assertions. • espresso-contrib Support for DatePicker, RecyclerView, Drawer and more. • espresso-idling-resource Mechanism that allows additional test- app synchronisation.
  4. Espresso includes: • espresso-core Core and basic View matchers, actions,

    and assertions. • espresso-intents Adds possibility to to validate and stub Intents. • espresso-contrib Support for DatePicker, RecyclerView, Drawer and more. • espresso-idling-resource Mechanism that allows additional test- app synchronisation.
  5. Espresso includes: • espresso-core Core and basic View matchers, actions,

    and assertions. • espresso-intents Adds possibility to to validate and stub Intents. • espresso-contrib Support for DatePicker, RecyclerView, Drawer and more. • espresso-web WebView support. • espresso-idling-resource Mechanism that allows additional test- app synchronisation.
  6. Root Activity view structure @+id/wrappedEditText1 @+id/wrappedEditText2 @+id/btnValidate @+id/etValue @+id/etValue FrameLayout

    FrameLayout EditText EditText Button <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:tools="http://schemas.android.com/tools"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:orientation="vertical"
 tools:context=".ui.activity.ExampleViewsActivity">
 
 <com.azimolabs.f1sherkk.espressoexamples.views.WrappedEditText
 android:id="@+id/wrappedEditText1"
 android:layout_width="240dp"
 android:layout_height="48dp"
 android:layout_gravity="center"
 android:layout_margin="16dp" />
 
 <com.azimolabs.f1sherkk.espressoexamples.views.WrappedEditText
 android:id="@+id/wrappedEditText2"
 android:layout_width="240dp"
 android:layout_height="48dp"
 android:layout_gravity="center"
 android:layout_margin="16dp" />
 
 <Button
 android:id="@+id/btnValidate"
 android:layout_width="160dp"
 android:layout_height="40dp"
 android:layout_gravity="center"
 android:text="Validate" />
 
 </LinearLayout>
  7. Root Examples: ViewMatcher @+id/wrappedEditText1 @+id/wrappedEditText2 @+id/btnValidate @+id/etValue @+id/etValue FrameLayout FrameLayout

    EditText EditText Button onView(withId(R.id.btnValidate)); onView(withId(R.id.wrappedEditText1)); 
 onView(withId(R.id.etValue));
 onView(allOf( withId(R.id.etValue),
 isDescendantOfA(
 withId(R.id.wrappedEditText1))
 ));
  8. Custom ViewMatcher 
 onView(allOf( withId(R.id.etValue),
 isDescendantOfA(
 withId(R.id.wrappedEditText1))
 )); public static

    Matcher<View> wrappedEditTextWithId(final int id) {
 return new BoundedMatcher<View, View>(View.class) {
 @Override
 public void describeTo(Description description) {
 description.appendText("EditText inside WrappedEditText class with id " + id);
 }
 
 @Override
 protected boolean matchesSafely(View view) {
 return view.getParent() instanceof WrappedEditText
 && ((WrappedEditText) view.getParent()).getId() == id
 && view.getId() == R.id.etValue;
 }
 };
 }
  9. Custom ViewMatcher 
 onView(allOf( withId(R.id.etValue),
 isDescendantOfA(
 withId(R.id.wrappedEditText1))
 )); public static

    Matcher<View> wrappedEditTextWithId(final int id) {
 return new BoundedMatcher<View, View>(View.class) {
 @Override
 public void describeTo(Description description) {
 description.appendText("EditText inside WrappedEditText class with id " + id);
 }
 
 @Override
 protected boolean matchesSafely(View view) {
 return view.getParent() instanceof WrappedEditText
 && ((WrappedEditText) view.getParent()).getId() == id
 && view.getId() == R.id.etValue;
 }
 };
 }
  10. Custom ViewMatcher 
 onView(allOf( withId(R.id.etValue),
 isDescendantOfA(
 withId(R.id.wrappedEditText1))
 )); public static

    Matcher<View> wrappedEditTextWithId(final int id) {
 return new BoundedMatcher<View, View>(View.class) {
 @Override
 public void describeTo(Description description) {
 description.appendText("EditText inside WrappedEditText class with id " + id);
 }
 
 @Override
 protected boolean matchesSafely(View view) {
 return view.getParent() instanceof WrappedEditText
 && ((WrappedEditText) view.getParent()).getId() == id
 && view.getId() == R.id.etValue;
 }
 };
 }
  11. Custom ViewMatcher 
 onView(allOf( withId(R.id.etValue),
 isDescendantOfA(
 withId(R.id.wrappedEditText1))
 )); public static

    Matcher<View> wrappedEditTextWithId(final int id) {
 return new BoundedMatcher<View, View>(View.class) {
 @Override
 public void describeTo(Description description) {
 description.appendText("EditText inside WrappedEditText class with id " + id);
 }
 
 @Override
 protected boolean matchesSafely(View view) {
 return view.getParent() instanceof WrappedEditText
 && ((WrappedEditText) view.getParent()).getId() == id
 && view.getId() == R.id.etValue;
 }
 };
 }
  12. Custom ViewMatcher 
 onView(allOf( withId(R.id.etValue),
 isDescendantOfA(
 withId(R.id.wrappedEditText1))
 )); public static

    Matcher<View> wrappedEditTextWithId(final int id) {
 return new BoundedMatcher<View, View>(View.class) {
 @Override
 public void describeTo(Description description) {
 description.appendText("EditText inside WrappedEditText class with id " + id);
 }
 
 @Override
 protected boolean matchesSafely(View view) {
 return view.getParent() instanceof WrappedEditText
 && ((WrappedEditText) view.getParent()).getId() == id
 && view.getId() == R.id.etValue;
 }
 };
 }
  13. Custom ViewMatcher 
 onView(allOf( withId(R.id.etValue),
 isDescendantOfA(
 withId(R.id.wrappedEditText1))
 )); public static

    Matcher<View> wrappedEditTextWithId(final int id) {
 return new BoundedMatcher<View, View>(View.class) {
 @Override
 public void describeTo(Description description) {
 description.appendText("EditText inside WrappedEditText class with id " + id);
 }
 
 @Override
 protected boolean matchesSafely(View view) {
 return view.getParent() instanceof WrappedEditText
 && ((WrappedEditText) view.getParent()).getId() == id
 && view.getId() == R.id.etValue;
 }
 };
 }
  14. Custom ViewMatcher 
 onView(wrappedEditTextWithId(R.id.wrappedEditText1)); public static Matcher<View> wrappedEditTextWithId(final int id)

    {
 return new BoundedMatcher<View, View>(View.class) {
 @Override
 public void describeTo(Description description) {
 description.appendText("EditText inside WrappedEditText class with id " + id);
 }
 
 @Override
 protected boolean matchesSafely(View view) {
 return view.getParent() instanceof WrappedEditText
 && ((WrappedEditText) view.getParent()).getId() == id
 && view.getId() == R.id.etValue;
 }
 };
 }
  15. @Test
 public void test_onValidateButtonClick_shouldCheckIfEditTextsAreFilled() {
 onView(withId(R.id.btnValidate)).perform(click());
 
 onView(wrappedEditTextWithId(R.id.wrappedEditText1))
 .check(matches(hasError(true)));
 


    onView(wrappedEditTextWithId(R.id.wrappedEditText2))
 .check(matches(hasError(false)));
 } public static Matcher<View> wrappedEditTextWithId(final int id) {
 return new BoundedMatcher<View, View>(View.class) {
 @Override
 public void describeTo(Description description) {
 description.appendText("EditText inside " +
 "WrappedEditText class with id " + id);
 }
 
 @Override
 protected boolean matchesSafely(View view) {
 return view.getParent() instanceof WrappedEditText
 && ((WrappedEditText) view.getParent()).getId() == id
 && view.getId() == R.id.etValue;
 }
 };
 }
  16. Root Activity view structure @+id/listview1 @+id/listview2 @+id/tvCurrentPickInfo ListView ListView TextView

    @+id/ tvItemNumber @+id/ btnPickMe Item TextView Button @+id/ tvItemNumber @+id/ btnPickMe Item TextView Button
  17. Root Example: DataOptions @+id/listview1 @+id/listview2 @+id/tvCurrentPickInfo ListView ListView TextView @+id/

    tvItemNumber @+id/ btnPickMe Item TextView Button @+id/ tvItemNumber @+id/ btnPickMe Item TextView Button onData(???);
  18. Example: DataOptions public static Matcher<Object> withValue(final int value) {
 return

    new BoundedMatcher<Object, Integer>(Integer.class) {
 @Override
 public void describeTo(Description description) {
 description.appendText("Object should be " +
 "integer with value " + value);
 }
 
 @Override
 protected boolean matchesSafely(Integer integer) {
 return integer == value;
 }
 };
 } public class NumbersAdapter extends ArrayAdapter<Integer> {
 
 (…)
 }
  19. Example: DataOptions public static Matcher<Object> withValue(final int value) {
 return

    new BoundedMatcher<Object, Integer>(Integer.class) {
 @Override
 public void describeTo(Description description) {
 description.appendText("Object should be " +
 "integer with value " + value);
 }
 
 @Override
 protected boolean matchesSafely(Integer integer) {
 return integer == value;
 }
 };
 } public class NumbersAdapter extends ArrayAdapter<Integer> {
 
 (…)
 }
  20. Root Example: DataOptions @+id/listview1 @+id/listview2 @+id/tvCurrentPickInfo ListView ListView TextView @+id/

    tvItemNumber @+id/ btnPickMe Item TextView Button @+id/ tvItemNumber @+id/ btnPickMe Item TextView Button onData(withValue(55)) With tvItemNumber value 55 With tvItemNumber value 55
  21. Root Example: DataOptions @+id/listview1 @+id/listview2 @+id/tvCurrentPickInfo ListView ListView TextView @+id/

    tvItemNumber @+id/ btnPickMe Item TextView Button @+id/ tvItemNumber @+id/ btnPickMe Item TextView Button onData(withValue(55)) .inAdapterView(withId(R.id.listView1)); With tvItemNumber value 55
  22. Root Example: DataOptions @+id/listview1 @+id/listview2 @+id/tvCurrentPickInfo ListView ListView TextView @+id/

    tvItemNumber @+id/ btnPickMe Item TextView Button @+id/ tvItemNumber @+id/ btnPickMe Item TextView Button onData(withValue(55)) .inAdapterView(withId(R.id.listView1)) .onChildView(withId(R.id.btnPickMe)); With tvItemNumber value 55
  23. RecyclerView java.lang.Object ↳ android.view.View ↳ android.view.ViewGroup ↳ android.support.v7.widget.RecyclerView java.lang.Object ↳

    android.view.View ↳ android.view.ViewGroup ↳ android.widget.AdapterView<android.widget.ListAdapter> ↳ android.widget.AbsListView ↳ android.widget.ListView
  24. Test process: Structure app.apk app-android- test.apk assemble assemble IDE Application

    APK Application Test APK install install Android Device
  25. Test process: Structure Test Process Application Package Test Package Test

    Cases Test Tools JUnit Instrumentation UIThread TestThread
  26. Test process: Structure Test Process Application Package Test Package Test

    Cases Test Tools JUnit Instrumentation Instrumentation Runner UIThread TestThread
  27. Example: Synchronisation @Test
 public void testFoo() {
 onData(withValue(999))
 .inAdapterView(withId(R.id.listView1))
 .onChildView(withId(R.id.btnPickMe))


    .perform(click());
 
 onData(withValue(999))
 .inAdapterView(withId(R.id.listView2))
 .onChildView(withId(R.id.btnPickMe))
 .perform(click());
 
 onData(withValue(1))
 .inAdapterView(withId(R.id.listView1))
 .onChildView(withId(R.id.btnPickMe))
 .perform(click());
 }
  28. Inside ViewInteraction.java private void doPerform(final ViewAction viewAction) {
 checkNotNull(viewAction);
 final

    Matcher<? extends View> constraints = checkNotNull(viewAction.getConstraints());
 runSynchronouslyOnUiThread(new Runnable() {
 
 @Override
 public void run() {
 uiController.loopMainThreadUntilIdle();
 View targetView = viewFinder.getView();
 Log.i(TAG, String.format(
 "Performing '%s' action on view %s", viewAction.getDescription(), viewMatcher));
 if (!constraints.matches(targetView)) {
 // TODO(user): update this to describeMismatch once hamcrest is updated to new
 StringDescription stringDescription = new StringDescription(new StringBuilder(
 "Action will not be performed because the target view "
 + "does not match one or more of the following constraints:\n"));
 constraints.describeTo(stringDescription);
 stringDescription.appendText("\nTarget view: ")
 .appendValue(HumanReadables.describe(targetView));
 
 if (viewAction instanceof ScrollToAction
 && isDescendantOfA(isAssignableFrom((AdapterView.class))).matches(targetView)) {
 stringDescription.appendText(
 "\nFurther Info: ScrollToAction on a view inside an AdapterView will not work. "
 + "Use Espresso.onData to load the view.");
 }
 throw new PerformException.Builder()
 .withActionDescription(viewAction.getDescription())
 .withViewDescription(viewMatcher.toString())
 .withCause(new RuntimeException(stringDescription.toString()))
 .build();
 } else {
 viewAction.perform(uiController, targetView);
 }
 }
 });
 }
  29. Inside ViewInteraction.java private void doPerform(final ViewAction viewAction) {
 (…) NULL

    CHECK CODE 
 runSynchronouslyOnUiThread(new Runnable() {
 
 @Override
 public void run() {
 uiController.loopMainThreadUntilIdle();
 (…) SOME OPERATION ON APPLICATION }
 });
 }
  30. Inside ViewInteraction.java private void doPerform(final ViewAction viewAction) {
 (…) NULL

    CHECK CODE 
 runSynchronouslyOnUiThread(new Runnable() {
 
 @Override
 public void run() {
 uiController.loopMainThreadUntilIdle();
 (…) SOME OPERATION ON APPLICATION }
 });
 } public ViewInteraction check(final ViewAssertion viewAssert) {
 checkNotNull(viewAssert);
 runSynchronouslyOnUiThread(new Runnable() {
 @Override
 public void run() {
 uiController.loopMainThreadUntilIdle();
 
 View targetView = null;
 NoMatchingViewException missingViewException = null;
 try {
 targetView = viewFinder.getView();
 } catch (NoMatchingViewException nsve) {
 missingViewException = nsve;
 }
 viewAssert.check(targetView, missingViewException);
 }
 });
 return this;
 }
  31. Synchronisation methods private void doPerform(final ViewAction viewAction) {
 (…) NULL

    CHECK CODE 
 runSynchronouslyOnUiThread(new Runnable() {
 
 @Override
 public void run() {
 uiController.loopMainThreadUntilIdle();
 (…) SOME OPERATION ON APPLICATION }
 });
 } public ViewInteraction check(final ViewAssertion viewAssert) {
 (…) NULL CHECK CODE 
 runSynchronouslyOnUiThread(new Runnable() { 
 @Override
 public void run() {
 uiController.loopMainThreadUntilIdle();
 
 (…) SOME KIND OF CHECK PERFORMED ON APPLICATION
 }
 });
 return this;
 }
  32. private void runSynchronouslyOnUiThread(Runnable action) {
 FutureTask<Void> uiTask = new FutureTask<Void>(action,

    null);
 mainThreadExecutor.execute(uiTask);
 try {
 uiTask.get();
 } catch (InterruptedException ie) {
 throw new RuntimeException("Interrupted running UI task", ie);
 } catch (ExecutionException ee) {
 failureHandler.handle(ee.getCause(), viewMatcher);
 }
 } @Inject
 ViewInteraction(
 UiController uiController,
 ViewFinder viewFinder,
 @MainThread Executor mainThreadExecutor,
 FailureHandler failureHandler,
 Matcher<View> viewMatcher,
 AtomicReference<Matcher<Root>> rootMatcherRef) {
 this.viewFinder = checkNotNull(viewFinder);
 this.uiController = checkNotNull(uiController);
 this.failureHandler = checkNotNull(failureHandler);
 this.mainThreadExecutor = checkNotNull(mainThreadExecutor);
 this.viewMatcher = checkNotNull(viewMatcher);
 this.rootMatcherRef = checkNotNull(rootMatcherRef);
 } Inside ViewInteraction.java
  33. private void runSynchronouslyOnUiThread(Runnable action) {
 FutureTask<Void> uiTask = new FutureTask<Void>(action,

    null);
 mainThreadExecutor.execute(uiTask);
 try {
 uiTask.get();
 } catch (InterruptedException ie) {
 throw new RuntimeException("Interrupted running UI task", ie);
 } catch (ExecutionException ee) {
 failureHandler.handle(ee.getCause(), viewMatcher);
 }
 } @Inject
 ViewInteraction(
 UiController uiController,
 ViewFinder viewFinder,
 @MainThread Executor mainThreadExecutor,
 FailureHandler failureHandler,
 Matcher<View> viewMatcher,
 AtomicReference<Matcher<Root>> rootMatcherRef) {
 this.viewFinder = checkNotNull(viewFinder);
 this.uiController = checkNotNull(uiController);
 this.failureHandler = checkNotNull(failureHandler);
 this.mainThreadExecutor = checkNotNull(mainThreadExecutor);
 this.viewMatcher = checkNotNull(viewMatcher);
 this.rootMatcherRef = checkNotNull(rootMatcherRef);
 } Inside ViewInteraction.java
  34. private void runSynchronouslyOnUiThread(Runnable action) {
 FutureTask<Void> uiTask = new FutureTask<Void>(action,

    null);
 mainThreadExecutor.execute(uiTask);
 try {
 uiTask.get();
 } catch (InterruptedException ie) {
 throw new RuntimeException("Interrupted running UI task", ie);
 } catch (ExecutionException ee) {
 failureHandler.handle(ee.getCause(), viewMatcher);
 }
 } @Inject
 ViewInteraction(
 UiController uiController,
 ViewFinder viewFinder,
 @MainThread Executor mainThreadExecutor,
 FailureHandler failureHandler,
 Matcher<View> viewMatcher,
 AtomicReference<Matcher<Root>> rootMatcherRef) {
 this.viewFinder = checkNotNull(viewFinder);
 this.uiController = checkNotNull(uiController);
 this.failureHandler = checkNotNull(failureHandler);
 this.mainThreadExecutor = checkNotNull(mainThreadExecutor);
 this.viewMatcher = checkNotNull(viewMatcher);
 this.rootMatcherRef = checkNotNull(rootMatcherRef);
 } Inside ViewInteraction.java
  35. private void runSynchronouslyOnUiThread(Runnable action) {
 FutureTask<Void> uiTask = new FutureTask<Void>(action,

    null);
 mainThreadExecutor.execute(uiTask);
 try {
 uiTask.get(); // BLOCKING!
 } catch (InterruptedException ie) {
 throw new RuntimeException("Interrupted running UI task", ie);
 } catch (ExecutionException ee) {
 failureHandler.handle(ee.getCause(), viewMatcher);
 }
 } @Inject
 ViewInteraction(
 UiController uiController,
 ViewFinder viewFinder,
 @MainThread Executor mainThreadExecutor,
 FailureHandler failureHandler,
 Matcher<View> viewMatcher,
 AtomicReference<Matcher<Root>> rootMatcherRef) {
 this.viewFinder = checkNotNull(viewFinder);
 this.uiController = checkNotNull(uiController);
 this.failureHandler = checkNotNull(failureHandler);
 this.mainThreadExecutor = checkNotNull(mainThreadExecutor);
 this.viewMatcher = checkNotNull(viewMatcher);
 this.rootMatcherRef = checkNotNull(rootMatcherRef);
 } Inside ViewInteraction.java
  36. Test Process Application Package Test Package TestThread UIThread 1. Injects

    task to Application main thread executor. 2. Has reference to sent task and waits for result.
  37. Synchronisation methods private void doPerform(final ViewAction viewAction) {
 (…) NULL

    CHECK CODE 
 runSynchronouslyOnUiThread(new Runnable() {
 
 @Override
 public void run() {
 uiController.loopMainThreadUntilIdle();
 (…) SOME OPERATION ON APPLICATION }
 });
 } public ViewInteraction check(final ViewAssertion viewAssert) {
 (…) NULL CHECK CODE 
 runSynchronouslyOnUiThread(new Runnable() { 
 @Override
 public void run() {
 uiController.loopMainThreadUntilIdle();
 
 (…) SOME KIND OF CHECK PERFORMED ON APPLICATION
 }
 });
 return this;
 }
  38. Test Process Application Package Test Package TestThread UIThread 1. Injects

    task to tested application main thread executor. 2. Has reference to sent task and waits for result. 3. Sent task monitors tested application and waits until it becomes “idle”. After that it executes code sent from test package.
  39. Inside UiController.java 
 public interface UiController {
 
 boolean injectMotionEvent(MotionEvent

    event) throws InjectEventSecurityException;
 
 boolean injectKeyEvent(KeyEvent event) throws InjectEventSecurityException;
 
 boolean injectString(String str) throws InjectEventSecurityException;
 
 void loopMainThreadUntilIdle();
 
 void loopMainThreadForAtLeast(long millisDelay);
 }
  40. Inside UiControllerImpl.java void loopMainThreadUntilIdle(); // Called always (during perform() or

    check()) IdlingConditions handled: • ASYNC_TASK_HAVE_IDLED // waits for asyncTask • COMPAT_TASKS_HAVE_IDLED // waits for asyncTask from AppCompat library • DYNAMIC_TASKS_HAVE_IDLED // waits for IdlingResources - user created waits Method which perform waits: uiController.loopUntil(EnumSet<IdleCondition> conditions);
  41. Inside UiControllerImpl.java QueueState queueState = queueInterrogator.determineQueueState();
 if (queueState == QueueState.EMPTY

    || queueState == QueueState.TASK_DUE_LONG) {
 return;
 } Inside loopUntil(); - waits for sent IdlingConditions - has access to applications main looper - can see task queue - 15 milliseconds ahead (QueueInterrogator.java) - allows task sent by Espresso to run • queue state is empty • pending tasks will start in very long time in the future void loopMainThreadUntilIdle(); // Called always (during perform() or check()) IdlingConditions handled: • ASYNC_TASK_HAVE_IDLED // waits for asyncTask • COMPAT_TASKS_HAVE_IDLED // waits for asyncTask from AppCompat library • DYNAMIC_TASKS_HAVE_IDLED // waits for IdlingResources - user created waits Method which perform waits: uiController.loopUntil(EnumSet<IdleCondition> conditions);
  42. Idle state is when: - there is no AsyncTask running

    - there is no AsyncCompatTask running - all IdlingResources sent by test author has idled - main looper’s queue is empty - main looper’s queue has pending tasks scheduled for distant future
  43. Inside UiControllerImpl.java boolean injectMotionEvent(MotionEvent event) throws InjectEventSecurityException; 1. Passed in

    ViewAction (which performs MotionEvent) to perform() method: * uiController.loopMainThreadUntilIdle(); called - wait for AsyncTask - wait for AsyncCompatTask - wait for registered IdlingResources - wait for main looper queue (empty or no pending tasks in near future) 2. Schedules task with MotionEvent code to executor 3. MotionEvent is currently in progress, and in background: * uiController.loopUntil(IdleCondition condition); called - wait until MotionEvent is finished ( enum: MOTION_INJECTION_HAS_COMPLETED ) - wait for main looper queue (empty or no pending tasks in near future) 4. Calls .get() result on scheduled task only after it finished performing to avoid blocking main thread. Additionally after gets result: * uiController.loopMainThreadUntilIdle(); called - wait for AsyncTask - wait for AsyncCompatTask - wait for registered IdlingResources - wait for main looper queue (empty or no pending tasks in near future)
  44. Inside UiControllerImpl.java boolean injectString(String str) throws InjectEventSecurityException; 1. Passed in

    ViewAction (which performs text injection) to perform() method: * uiController.loopMainThreadUntilIdle(); called - wait for AsyncTask - wait for AsyncCompatTask - wait for registered IdlingResources - wait for main looper queue (empty or no pending tasks in near future) 2. Maps sent String into array of KeyEvents (one KeyEvent per char) 3. Invokes in for loop injectKeyEvent(KeyEvent event) method for each KeyEvent
  45. Inside UiControllerImpl.java boolean injectKeyEvent(KeyEvent event) throws InjectEventSecurityException; 1. It is

    meant to be used in loop, so it needs to keep checking if something haven’t changed since finish of last iteration: * uiController.loopMainThreadUntilIdle(); called - wait for AsyncTask - wait for AsyncCompatTask - wait for registered IdlingResources - wait for main looper queue (empty or no pending tasks in near future) 2. Schedules task with KeyEvent code to executor 3. KeyEvent is currently in progress, and in background: * uiController.loopUntil(IdleCondition condition); called - wait until MotionEvent is finished ( enum: KEY_INJECT_HAS_COMPLETED ) - wait for main looper queue (empty or no pending tasks in near future) 4. Calls .get() result on scheduled task only after it finished performing to avoid blocking main thread.
  46. Inside UiControllerImpl.java void loopMainThreadForAtLeast(long millisDelay); 1. It is looping the

    main thread for long millisDelay milliseconds which prolongs until test thread receives back the control 2. Calls: * uiController.loopMainThreadUntilIdle(); called - wait for AsyncTask - wait for AsyncCompatTask - wait for registered IdlingResources - wait for main looper queue (empty or no pending tasks in near future)
  47. Loop/Wait but for how long? IdlingPolicies.java 
 private static volatile

    IdlingPolicy masterIdlingPolicy = new IdlingPolicy.Builder()
 .withIdlingTimeout(60)
 .withIdlingTimeoutUnit(TimeUnit.SECONDS)
 .throwAppNotIdleException()
 .build(); /**
 * Updates the IdlingPolicy used in UiController.loopUntil to detect AppNotIdleExceptions.
 *
 * @param timeout the timeout before an AppNotIdleException is created.
 * @param unit the unit of the timeout value.
 */
 public static void setMasterPolicyTimeout(long timeout, TimeUnit unit) {
 checkArgument(timeout > 0);
 checkNotNull(unit);
 masterIdlingPolicy = masterIdlingPolicy.toBuilder()
 .withIdlingTimeout(timeout)
 .withIdlingTimeoutUnit(unit)
 .build();
 }
  48. Example of problem public int generateDelay(int minValueInMillis, int maxValueInMillis) {


    return new Random().nextInt(maxValueInMillis - minValueInMillis) + minValueInMillis;
 } public void loadDataWithDelay(int delayInMillis) {
 new Handler().postDelayed(new Runnable() {
 @Override
 public void run() {
 adapter.clear();
 adapter.addAll(DataProvider.generateServerList());
 adapter.notifyDataSetChanged();
 }
 }, delayInMillis);
 } @Override
 protected void onStart() {
 super.onStart();
 loadDataWithDelay(generateDelay(1000, 5000));
 }
  49. Example of problem @Test
 public void testFoo() {
 onData(anything()).inAdapterView(withId(R.id.lvList)).atPosition(2).perform(click());
 }

    android.support.test.espresso.PerformException: Error performing 'load adapter data' on view 'with id: com.azimolabs.f1sherkk.espressoexamples:id/lvList'.
  50. public class ExampleIdlingResource implements IdlingResource {
 
 @Override
 public String

    getName() {
 return null;
 }
 
 @Override
 public boolean isIdleNow() {
 return false;
 }
 
 @Override
 public void registerIdleTransitionCallback(ResourceCallback callback) {
 
 }
 }
  51. public class ExampleIdlingResource implements IdlingResource {
 
 @Override
 public String

    getName() {
 return null;
 }
 
 @Override
 public boolean isIdleNow() {
 return false;
 }
 
 @Override
 public void registerIdleTransitionCallback(ResourceCallback callback) {
 
 }
 }
  52. IdlingResourceRegistry.java It doesn’t allow to register IdlingResources with the same

    names. for (IdlingResource resource : resourceList) {
 checkNotNull(resource.getName(), "IdlingResource.getName() should not be null");
 
 boolean duplicate = false;
 for (IdlingResource oldResource : resources) {
 if (resource.getName().equals(oldResource.getName())) {
 // This does not throw an error to avoid leaving tests that register resource in test
 // setup in an undeterministic state (we cannot assume that everyone clears vm state
 // between each test run)
 Log.e(TAG, String.format("Attempted to register resource with same names:"
 + " %s. R1: %s R2: %s.\nDuplicate resource registration will be ignored.",
 resource.getName(), resource, oldResource));
 duplicate = true;
 break;
 }
 }
 
 if (!duplicate) {
 resources.add(resource);
 final int position = resources.size() - 1;
 registerToIdleCallback(resource, position);
 idleState.set(position, resource.isIdleNow());
 } else {
 allRegisteredSuccesfully = false;
 }
  53. public class ExampleIdlingResource implements IdlingResource {
 
 @Override
 public String

    getName() {
 return ExampleIdlingResource.class.getName();
 }
 
 @Override
 public boolean isIdleNow() {
 return false;
 }
 
 @Override
 public void registerIdleTransitionCallback(ResourceCallback callback) {
 
 }
 }
  54. public class ExampleIdlingResource implements IdlingResource {
 
 @Override
 public String

    getName() {
 return ExampleIdlingResource.class.getName();
 }
 
 @Override
 public boolean isIdleNow() {
 return false;
 }
 
 @Override
 public void registerIdleTransitionCallback(ResourceCallback callback) {
 
 }
 }
  55. public class ExampleIdlingResource implements IdlingResource {
 private ResourceCallback resourceCallback; 


    @Override
 public String getName() {
 return ExampleIdlingResource.class.getName();
 }
 
 @Override
 public boolean isIdleNow() {
 return false;
 }
 
 @Override
 public void registerIdleTransitionCallback(ResourceCallback callback) {
 this.resourceCallback = callback;
 }
 }
  56. public class ExampleIdlingResource implements IdlingResource {
 private ResourceCallback resourceCallback; 


    @Override
 public String getName() {
 return ExampleIdlingResource.class.getName();
 }
 
 @Override
 public boolean isIdleNow() {
 return false;
 }
 
 @Override
 public void registerIdleTransitionCallback(ResourceCallback callback) {
 this.resourceCallback = callback;
 }
 }
  57. public class ExampleIdlingResource implements IdlingResource { private ResourceCallback resourceCallback; private

    ActivityTestRule rule; public ExampleIdlingResource(ActivityTestRule rule) {
 this.rule = rule;
 } 
 @Override
 public String getName() {
 return ExampleIdlingResource.class.getName();
 }
 
 @Override
 public boolean isIdleNow() {
 return false;
 }
 
 @Override
 public void registerIdleTransitionCallback(ResourceCallback callback) {
 this.resourceCallback = callback;
 }
 }
  58. public class ExampleIdlingResource implements IdlingResource { private ResourceCallback resourceCallback; private

    ActivityTestRule rule; public ExampleIdlingResource(ActivityTestRule rule) {
 this.rule = rule;
 } 
 @Override
 public String getName() {
 return ExampleIdlingResource.class.getName();
 }
 
 @Override
 public boolean isIdleNow() { ListView serverList = (ListView) rule.getActivity().findViewById(R.id.lvList); boolean isIdle = !serverList.getAdapter().isEmpty(); 
 return false;
 }
 
 @Override
 public void registerIdleTransitionCallback(ResourceCallback callback) {
 this.resourceCallback = callback;
 }
 }
  59. public class ExampleIdlingResource implements IdlingResource { private ResourceCallback resourceCallback; private

    ActivityTestRule rule; public ExampleIdlingResource(ActivityTestRule rule) {
 this.rule = rule;
 } 
 @Override
 public String getName() {
 return ExampleIdlingResource.class.getName();
 }
 
 @Override
 public boolean isIdleNow() { ListView serverList = (ListView) rule.getActivity().findViewById(R.id.lvList); boolean isIdle = !serverList.getAdapter().isEmpty(); if (isIdle) {
 resourceCallback.onTransitionToIdle();
 } 
 return isIdle;
 }
 
 @Override
 public void registerIdleTransitionCallback(ResourceCallback callback) {
 this.resourceCallback = callback;
 }
 }
  60. public class ExampleIdlingResource implements IdlingResource { private ResourceCallback resourceCallback; private

    ActivityTestRule rule; private boolean isIdle; public ExampleIdlingResource(ActivityTestRule rule) {
 this.rule = rule;
 } 
 @Override
 public String getName() {
 return ExampleIdlingResource.class.getName();
 }
 
 @Override
 public boolean isIdleNow() { if (isIdle) return true; ListView serverList = (ListView) rule.getActivity().findViewById(R.id.lvList); isIdle = !serverList.getAdapter().isEmpty(); if (isIdle) {
 resourceCallback.onTransitionToIdle();
 } 
 return isIdle;
 }
 
 @Override
 public void registerIdleTransitionCallback(ResourceCallback callback) {
 this.resourceCallback = callback;
 }
 }
  61. IdlingResource usage @Test
 public void testFoo() { ExampleIdlingResource exampleIdlingResource =

    new ExampleIdlingResource(activityRule); Espresso.registerIdlingResources(exampleIdlingResource);
 onData(anything()).inAdapterView(withId(R.id.lvList)).atPosition(2).perform(click()); Espresso.unregisterIdlingResources(exampleIdlingResource);
 } • Espresso.registerIdlingResources((IdlingResource... resources) • Espresso.unregisterIdlingResources((IdlingResource... resources)
  62. IdlingResourceRegistry.java It’s a singleton - state persists between test cases.

    @Singleton
 public final class IdlingResourceRegistry { (…) }
  63. isIdleNow() call interval @Override
 public boolean isIdleNow() {
 Log.d("Test-Log", "isIdleNow()

    called, isIdle: " +isIdle);
 if (isIdle) return true;
 
 ListView serverList = (ListView) rule.getActivity().findViewById(R.id.lvList);
 isIdle = !serverList.getAdapter().isEmpty();
 if (isIdle) {
 resourceCallback.onTransitionToIdle();
 }
 return isIdle;
 } 09-13 01:53:20.561 3072-3072/? D/Test-Log: isIdleNow() called, isIdle: false // 5 seconds 09-13 01:53:25.584 3072-3072/? D/Test-Log: isIdleNow() called, isIdle: false 09-13 01:53:25.585 3072-3072/? D/Test-Log: isIdleNow() called, isIdle: true // ~ 10-30 milliseconds 09-13 01:53:25.600 3072-3072/? D/Test-Log: isIdleNow() called, isIdle: true 09-13 01:53:25.600 3072-3072/? D/Test-Log: isIdleNow() called, isIdle: true 09-13 01:53:25.635 3072-3072/? D/Test-Log: isIdleNow() called, isIdle: true 09-13 01:53:25.635 3072-3072/? D/Test-Log: isIdleNow() called, isIdle: true 09-13 01:53:25.642 3072-3072/? D/Test-Log: isIdleNow() called, isIdle: true 09-13 01:53:25.643 3072-3072/? D/Test-Log: isIdleNow() called, isIdle: true 09-13 01:53:25.667 3072-3072/? D/Test-Log: isIdleNow() called, isIdle: true 09-13 01:53:25.667 3072-3072/? D/Test-Log: isIdleNow() called, isIdle: true 09-13 01:53:28.555 3072-3072/? D/Test-Log: isIdleNow() called, isIdle: true 09-13 01:53:28.555 3072-3072/? D/Test-Log: isIdleNow() called, isIdle: true 09-13 01:53:28.621 3072-3072/? D/Test-Log: isIdleNow() called, isIdle: true 09-13 01:53:28.621 3072-3072/? D/Test-Log: isIdleNow() called, isIdle: true
  64. isIdleNow() call interval @Override
 public boolean isIdleNow() {
 Log.d("Test-Log", "isIdleNow()

    called, isIdle: " +isIdle);
 if (isIdle) return true;
 
 ListView serverList = (ListView) rule.getActivity().findViewById(R.id.lvList);
 isIdle = !serverList.getAdapter().isEmpty();
 if (isIdle) {
 resourceCallback.onTransitionToIdle();
 }
 return isIdle;
 } IdlingPolicies.java private static volatile IdlingPolicy dynamicIdlingResourceWarningPolicy =
 new IdlingPolicy.Builder()
 .withIdlingTimeout(5)
 .withIdlingTimeoutUnit(TimeUnit.SECONDS)
 .logWarning()
 .build();
  65. isIdleNow() timeout IdlingPolicies.java 
 private static volatile IdlingPolicy dynamicIdlingResourceErrorPolicy =

    new IdlingPolicy.Builder()
 .withIdlingTimeout(26)
 .withIdlingTimeoutUnit(TimeUnit.SECONDS)
 .throwIdlingResourceTimeoutException()
 .build(); /**
 * Updates the IdlingPolicy used by IdlingResourceRegistry to determine when IdlingResources
 * timeout.
 *
 * @param timeout the timeout before an IdlingResourceTimeoutException is created.
 * @param unit the unit of the timeout value.
 */
 public static void setIdlingResourceTimeout(long timeout, TimeUnit unit) {
 checkArgument(timeout > 0);
 checkNotNull(unit);
 dynamicIdlingResourceErrorPolicy = dynamicIdlingResourceErrorPolicy.toBuilder()
 .withIdlingTimeout(timeout)
 .withIdlingTimeoutUnit(unit)
 .build();
 }