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

Espresso 2.0

Espresso 2.0

Meetup PAUG

Thomas Guerin

February 07, 2015
Tweet

More Decks by Thomas Guerin

Other Decks in Programming

Transcript

  1. AndroidJUnitRunner AndroidJUnitRunner @Tom404_ Contains all improvements of GoogleInstrumentationTestRunner: • Application

    onCreate has finished before testing begins • Instrumentation finished == Activities finished • Reliable Activity monitoring • New Features
  2. AndroidJUnitRunner New features @Tom404_ Test Filters Test Timeout Sharding of

    tests Activity monitoring mechanism ActivityLifecycleMonitorRegistry JUnit4 support
  3. AndroidJUnitRunner Test filters @Tom404_ @RequiresDevice @[Small | Medium | Large]Test

    @SdkSupress // Launch tests given a size adb shell am instrument -w -e size [small | medium | large] com.application.name/android.support.test.runner.AndroidJUnitRunner
  4. AndroidJUnitRunner Sharding @Tom404_ // Create 4 tests shards and run

    the second shard of tests adb shell am instrument -w -e numShards 4 -e shardIndex 1 com.application.name/android.support.test.runner.AndroidJUnitRunner
  5. AndroidJUnitRunner Activity monitoring @Tom404_ // Find activities given a stage

    (CREATED, STARTED, PAUSED, RESUMED, STOPPED, // DESTROYED) Set<Activity> pausedActivities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.PAUSED);
  6. AndroidJUnitRunner Activity monitoring @Tom404_ // Find activities given a stage

    (CREATED, STARTED, PAUSED, RESUMED, STOPPED, // DESTROYED) Set<Activity> pausedActivities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.PAUSED); // Get notified when activity lifecycle changes ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(callback) ActivityLifecycleMonitorRegistry.getInstance().removeLifecycleCallback(callback)
  7. Hamcrest Hamcrest Matchers library assertThat("Hello", equalTo("Hello")) // Syntactic sugar assertThat(“Hello",

    is("Hello")) // A lot of matchers available allOf not instanceOf hasProperty equalToIgnoringCase @Tom404_
  8. Hamcrest Hamcrest Matchers library assertThat("Hello", equalTo("Hello")) // Syntactic sugar assertThat(“Hello",

    is("Hello")) Easy to compose // A lot of matchers available allOf not instanceOf hasProperty equalToIgnoringCase @Tom404_
  9. Hamcrest Hamcrest custom matcher public class IsNotANumber extends TypeSafeMatcher<Double> {

    @Override public boolean matchesSafely(Double number) { return number.isNaN(); } public void describeTo(Description description) { description.appendText("not a number"); } public static Matcher<Double> notANumber() { return new IsNotANumber(); } } @Tom404_
  10. Espresso 1.x Example onView(allOf(withId(R.id.my_button), withText(“My Button”))) .check(matches(isDisplayed())) .perform(click()); // A

    simple extract makes It easier to read onView(withIdAndText(R.id.my_button, "My Button”)) .check(matches(isDisplayed())) .perform(click()); @Tom404_
  11. Espresso 1.x Example onView(allOf(withId(R.id.my_button), withText(“My Button”))) .check(matches(isDisplayed())) .perform(click()); // A

    simple extract makes It easier to read onView(withIdAndText(R.id.my_button, "My Button”)) .check(matches(isDisplayed())) .perform(click()); // If button is not displayed, perform(click()) will fail onView(withIdAndText(R.id.my_button, "My Button")).perform(click()); @Tom404_
  12. Espresso 1.x Example onView(allOf(withId(R.id.my_button), withText(“My Button”))) .check(matches(isDisplayed())) .perform(click()); Not enough

    restrictions == AmbiguousViewMatcherException // A simple extract makes It easier to read onView(withIdAndText(R.id.my_button, "My Button”)) .check(matches(isDisplayed())) .perform(click()); // If button is not displayed, perform(click()) will fail onView(withIdAndText(R.id.my_button, "My Button")).perform(click()); @Tom404_
  13. onData(allOf(is(instanceOf(String.class)), is("textToFind"))).perform(click()); // Or directly to a position onData(is(instanceOf(String.class))).atPosition(0).perform(click()); //

    Specify a listview, useful for viewPager onData(allOf(is(instanceOf(String.class)), is("textToFind"))) .inAdapterView(withId(R.id.my_list)).perform(click()); // Interaction with child view in a row onData(allOf(is(instanceOf(String.class)), is("textToFind"))) .onChildView(withId(R.id.my_child)) .perform(click()); Espresso 1.x Listview case @Tom404_
  14. // Type returned by adapter getItem method public class Item

    { public String name; public String itemContent; } More complex case onData(allOf(is(instanceOf(Item.class)), hasProperty(“name", equalTo(“nameToFind")))) .perform(click()); Espresso 1.x Listview case @Tom404_
  15. public static Matcher<Object> withItemName(final Matcher<String> itemTextMatcher){ return new BoundedMatcher<Object, Item>(Item.class)

    { @Override public boolean matchesSafely(Item item) { return itemTextMatcher.matches(item.name); } @Override public void describeTo(Description description) { description.appendText("with item name: "); itemTextMatcher.describeTo(description); } }; } Custom matcher onData(withItemName(equalTo("nameToFind"))).perform(click()); Espresso 1.x Listview case @Tom404_
  16. com.google.android.apps.common.testing.ui.espresso.NoMatchingViewException: No views in hierarchy found matching: with id: is

    <1234> If the target view is not part of the view hierarchy, you may need to use Espresso.onData to load it from one of the following AdapterViews:android.widget.ListView{426f6f18 VFED.VC. ......ID 0,0-720,1557} - android.widget.ListView{4269c598 V.ED.VC. ........ 48,393-1032,1509 #7f070042 app:id/my_list} View Hierarchy: +>DecorView{id=-1, visibility=VISIBLE, width=1080, height=1776, has-focus=true, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is- layout-requested=false, is-selected=false, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=1} | +->ActionBarOverlayLayout{id=16909075, res-name=action_bar_overlay_layout, visibility=VISIBLE, width=1080, height=1776, has-focus=true, has-focusable=true, has-window-focus=true, is- clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is- selected=false, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child- count=3} | +-->FrameLayout{id=16908290, res-name=content, visibility=VISIBLE, width=1080, height=1557, has-focus=true, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is- focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, root-is-layout- requested=false, has-input-connection=false, x=0.0, y=219.0, child-count=1} // Try to to find an unknown view onView(withId(1234)); Espresso 1.x Meaningful error logs @Tom404_
  17. Under the hood Goodbye flakyness ! For each test action

    Espresso will : 1. Wait until the app is idle 2. Run on the UI thread 3. Wait until completion 4. Check the result @Tom404_
  18. Under the hood Goodbye flakyness ! onView(withId(R.id.start_activity_button)).perform(click()); onView(withId(R.id.view_in_new_activity)).perform(click()); For each

    test action Espresso will : 1. Wait until the app is idle 2. Run on the UI thread 3. Wait until completion 4. Check the result @Tom404_
  19. Under the hood Goodbye flakyness ! onView(withId(R.id.start_activity_button)).perform(click()); onView(withId(R.id.view_in_new_activity)).perform(click()); For each

    test action Espresso will : 1. Wait until the app is idle 2. Run on the UI thread 3. Wait until completion 4. Check the result @Tom404_
  20. Use IdlingResource interface for custom implementation Under the hood Idle

    but active ! Espresso synchronizes AsyncTask default thread pool @Tom404_
  21. Under the hood Idle but active ! class EspressoThreadPool extends

    ThreadPoolExecutor implements IdlingResource { private int threadCount = 0; private ResourceCallback resourceCallback; ... @Override public synchronized void execute(Runnable r) { threadCount++; super.execute(r); } @Override public synchronized boolean isIdleNow() { return threadCount == 0; } @Override public void registerIdleTransitionCallback(ResourceCallback resourceCallback) { this.resourceCallback = resourceCallback; } @Override protected synchronized void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); threadCount--; if (resourceCallback != null && isIdleNow()) { resourceCallback.onTransitionToIdle(); } } } @Tom404_
  22. Under the hood Idle but active ! @Before public void

    setUp() { Espresso.registerIdlingResources(espressoThreadPool); } @Tom404_ @After public void tearDown() { Espresso.unregisterIdlingResources(espressoThreadPool); }
  23. Espresso 2.0 Other changes @Tom404_ PickerActions Switched target SDK 21

    Many new sample activities/tests RecyclerViewActions
  24. Espresso 2.0 Webview support @Tom404_ “While testing internally, we discovered

    some issues, which required major changes. We are putting the finishing touches on a new and improved API - it is coming shortly and it promises to be awesome.”
  25. Espresso 2.0 Webview support @Tom404_ “While testing internally, we discovered

    some issues, which required major changes. We are putting the finishing touches on a new and improved API - it is coming shortly and it promises to be awesome.”
  26. Tips Priority Job Queue @Tom404_ public abstract class BaseJob extends

    Job { public static final AtomicInteger COUNTER = new AtomicInteger(0); @Override public final void onAdded() { COUNTER.incrementAndGet(); } @Override public final void onRun() throws Throwable { run(); COUNTER.decrementAndGet(); } protected abstract void run() throws Throwable; @Override protected final void onCancel() { cancel(); COUNTER.decrementAndGet(); } }
  27. Tips Priority Job Queue @Tom404_ public class PriorityJobQueueIdleMonitor implements IdlingResource

    { private AtomicBoolean wasIdle = new AtomicBoolean(true); private volatile ResourceCallback resourceCallback; … @Override public boolean isIdleNow() { boolean isIdle = BaseJob.COUNTER.get() == 0; boolean wasIdle = this.wasIdle.getAndSet(isIdle); if (isIdle && !wasIdle && resourceCallback != null) { resourceCallback.onTransitionToIdle(); resourceCallback = null; } return isIdle; } }