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

Maintainable Espresso Tests with Robots and Screenshots

Maintainable Espresso Tests with Robots and Screenshots

AnDevCon 2017 - Washington DC
VIDEO: https://www.youtube.com/watch?v=qvzlQB6aieY

Sam Edwards

July 17, 2017
Tweet

More Decks by Sam Edwards

Other Decks in Programming

Transcript

  1. The Dream Espresso Test Suite Easy to write Low maintenance

    UI tests added during the development cycle Hermetic, deterministic, targeted Run on every PR in Continuous Integration Reliable test execution environment Automated screenshots
  2. Espresso Example @Test public void testLogin() { onView(withText(“Shopping App”))
 .check(matches(isDisplayed()));

    onView(withId(R.id.username))
 .perform(typeText("sam")); onView(withText(“LOG IN"))
 .perform(click()); }
  3. Espresso Example @Test public void testLogin() { onView(withText(“Shopping App”))
 .check(matches(isDisplayed()));

    onView(withId(R.id.username))
 .perform(typeText("sam")); onView(withText(“LOG IN"))
 .perform(click()); }
  4. Espresso Example @Test public void testLogin() { onView(withText(“Shopping App”))
 .check(matches(isDisplayed()));

    onView(withId(R.id.username))
 .perform(typeText("sam")); onView(withText(“LOG IN"))
 .perform(click()); }
  5. Espresso Test Recorder (Android Studio 2.2+) PROS: Great for beginners

    Great for projects without existing tests CONS: Only works on simple tests Tool is slow Generated code
  6. Who should write Espresso tests? “Espresso is targeted at developers,

    who believe that automated testing is an integral part of the development lifecycle. While it can be used for black-box testing, Espresso’s full power is unlocked by those who are familiar with the codebase under test.”
  7. When Should I write an Espresso test? Avoid unless necessary

    Only testing the UI Small and targeted
  8. How is Espresso Fast? Aware of Android’s Main Thread Aware

    of Android’s AsyncTask Thread Pool Idling Resources Never have to Thread.sleep(5000);
  9. new LoginRobot().username("sam").password("password").toggleRememberMe().login(); public class LoginRobot { public LoginRobot username(String username)

    { ViewInteraction usernameEditText = onView(allOf(withId(R.id.username), isDisplayed())); usernameEditText.perform(replaceText(username), closeSoftKeyboard()); return this; } public LoginRobot password(String password) { ViewInteraction passwordEditText = onView(allOf(withId(R.id.password), isDisplayed())); passwordEditText.perform(replaceText(password), closeSoftKeyboard()); return this; } public LoginRobot toggleRememberMe() { ViewInteraction rememberMeCheckbox = onView(allOf(withId(R.id.remember_me), isDisplayed())); rememberMeCheckbox.perform(scrollTo(), click()); return this; } public LoginRobot login() { ViewInteraction submitButton = onView(allOf(withId(R.id.submit), isDisplayed())); submitButton.perform(click()); return this; } }
  10. new LoginRobot().username("sam").password("password").toggleRememberMe().login(); public class LoginRobot { public LoginRobot username(String username)

    { ViewInteraction usernameEditText = onView(allOf(withId(R.id.username), isDisplayed())); usernameEditText.perform(replaceText(username), closeSoftKeyboard()); return this; } public LoginRobot password(String password) { ViewInteraction passwordEditText = onView(allOf(withId(R.id.password), isDisplayed())); passwordEditText.perform(replaceText(password), closeSoftKeyboard()); return this; } public LoginRobot toggleRememberMe() { ViewInteraction rememberMeCheckbox = onView(allOf(withId(R.id.remember_me), isDisplayed())); rememberMeCheckbox.perform(scrollTo(), click()); return this; } public LoginRobot login() { ViewInteraction submitButton = onView(allOf(withId(R.id.submit), isDisplayed())); submitButton.perform(click()); return this; } }
  11. new LoginRobot().username("sam").password("password").toggleRememberMe().login(); public class LoginRobot { public LoginRobot username(String username)

    { ViewInteraction usernameEditText = onView(allOf(withId(R.id.username), isDisplayed())); usernameEditText.perform(replaceText(username), closeSoftKeyboard()); return this; } public LoginRobot password(String password) { ViewInteraction passwordEditText = onView(allOf(withId(R.id.password), isDisplayed())); passwordEditText.perform(replaceText(password), closeSoftKeyboard()); return this; } public LoginRobot toggleRememberMe() { ViewInteraction rememberMeCheckbox = onView(allOf(withId(R.id.remember_me), isDisplayed())); rememberMeCheckbox.perform(scrollTo(), click()); return this; } public LoginRobot login() { ViewInteraction submitButton = onView(allOf(withId(R.id.submit), isDisplayed())); submitButton.perform(click()); return this; } }
  12. new LoginRobot().username("sam").password("password").toggleRememberMe().login(); public class LoginRobot { public LoginRobot username(String username)

    { ViewInteraction usernameEditText = onView(allOf(withId(R.id.username), isDisplayed())); usernameEditText.perform(replaceText(username), closeSoftKeyboard()); return this; } public LoginRobot password(String password) { ViewInteraction passwordEditText = onView(allOf(withId(R.id.password), isDisplayed())); passwordEditText.perform(replaceText(password), closeSoftKeyboard()); return this; } public LoginRobot toggleRememberMe() { ViewInteraction rememberMeCheckbox = onView(allOf(withId(R.id.remember_me), isDisplayed())); rememberMeCheckbox.perform(scrollTo(), click()); return this; } public LoginRobot login() { ViewInteraction submitButton = onView(allOf(withId(R.id.submit), isDisplayed())); submitButton.perform(click()); return this; } }
  13. HOME ROBOT ITEM
 ROBOT LOGIN ROBOT CATEGORY ROBOT new LoginRobot().username(“sam").password("password").toggleRememberMe().login();

    new HomeRobot().category(“Fruits”); new CategoryRobot().item(“Pineapple”); new ItemRobot().addToCart(); MULTI-SCREEN TESTS
  14. HOME ROBOT ITEM
 ROBOT LOGIN ROBOT CATEGORY ROBOT new LoginRobot().username(“sam").password("password").toggleRememberMe().login();

    new HomeRobot().category(“Fruits”); new CategoryRobot().item(“Pineapple”); new ItemRobot().addToCart(); MULTI-SCREEN TESTS
  15. HOME ROBOT ITEM
 ROBOT LOGIN ROBOT CATEGORY ROBOT new LoginRobot().username(“sam").password("password").toggleRememberMe().login();

    new HomeRobot().category(“Fruits”); new CategoryRobot().item(“Pineapple”); new ItemRobot().addToCart(); MULTI-SCREEN TESTS
  16. HOME ROBOT ITEM
 ROBOT LOGIN ROBOT CATEGORY ROBOT new LoginRobot().username(“sam").password("password").toggleRememberMe().login();

    new HomeRobot().category(“Fruits”); new CategoryRobot().item(“Pineapple”); new ItemRobot().addToCart(); MULTI-SCREEN TESTS
  17. HOME ROBOT ITEM
 ROBOT LOGIN ROBOT CATEGORY ROBOT new LoginRobot().username(“sam").password("password").toggleRememberMe().login();

    new HomeRobot().category(“Fruits”); new CategoryRobot().item(“Pineapple”); new ItemRobot().addToCart(); MULTI-SCREEN TESTS
  18. public class LoginRobot { public LoginRobot username(String username) { ViewInteraction

    usernameEditText = onView(allOf(withId(R.id.username), isDisplayed())); usernameEditText
 .perform(replaceText(username), closeSoftKeyboard()); takeScreenshot(“entered_username”); return this; } } SCREENSHOTS FOR EACH ROBOT METHOD
  19. public class LoginRobot { public LoginRobot login() { ViewInteraction submitButton

    = onView(allOf(withId(R.id.submit), isDisplayed())); takeScreenshot(“logging_in”); submitButton.perform(click()); return this; } } SCREENSHOTS FOR EACH ROBOT METHOD
  20. 2. Diagnose Failures android.support.test.espresso.NoMatchingViewException: No views in hierarchy found matching:

    with text: “DONE!” View Hierarchy: +>DecorView{id=-1, visibility=VISIBLE, width=720, height=1280, has-focus=false, has-focusable=false, 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} | +->ActionBarOverlayLayout{id=16909230, res-name=decor_content_parent, visibility=VISIBLE, width=720, height=1184, has-focus=false, has-focusable=false, 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=2} | +-->FrameLayout{id=16908290, res-name=content, visibility=VISIBLE, width=720, height=1024, has-focus=false, has-focusable=false, 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=160.0, child-count=1} | +--->TextView{id=2131230720, res-name=greeting, visibility=VISIBLE, width=656, height=960, has-focus=false, has-focusable=false, 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=32.0, y=32.0, text=Hello world!, input-type=0, ime-target=false, has-links=false} | +-->ActionBarContainer{id=16909231, res-name=action_bar_container, visibility=VISIBLE, width=720, height=112, has-focus=false, has-focusable=false, 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=48.0, child-count=2} | +--->Toolbar{id=16909232, res-name=action_bar, visibility=VISIBLE, width=720, height=112, has-focus=false, has-focusable=false, 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=2} | +---->TextView{id=-1, visibility=VISIBLE, width=206, height=54, has-focus=false, has-focusable=false, 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=32.0, y=29.0, text=Hello World, input-type=0, ime-target=false, has-links=false} | +---->ActionMenuView{id=-1, visibility=VISIBLE, width=80, height=112, has-focus=false, has-focusable=false, 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=640.0, y=0.0, child-count=1} | +----->OverflowMenuButton{id=-1, desc=More options, visibility=VISIBLE, width=80, height=96, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=true, is-enabled=true, is- focused=false, is-focusable=true, is-layout-requested=false, is-selected=false, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=8.0}
  21. 2. Diagnose Failures android.support.test.espresso.NoMatchingViewException: No views in hierarchy found matching:

    with text: “DONE!” View Hierarchy: +>DecorView{id=-1, visibility=VISIBLE, width=720, height=1280, has-focus=false, has-focusable=false, 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} | +->ActionBarOverlayLayout{id=16909230, res-name=decor_content_parent, visibility=VISIBLE, width=720, height=1184, has-focus=false, has-focusable=false, 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=2} | +-->FrameLayout{id=16908290, res-name=content, visibility=VISIBLE, width=720, height=1024, has-focus=false, has-focusable=false, 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=160.0, child-count=1} | +--->TextView{id=2131230720, res-name=greeting, visibility=VISIBLE, width=656, height=960, has-focus=false, has-focusable=false, 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=32.0, y=32.0, text=Hello world!, input-type=0, ime-target=false, has-links=false} | +-->ActionBarContainer{id=16909231, res-name=action_bar_container, visibility=VISIBLE, width=720, height=112, has-focus=false, has-focusable=false, 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=48.0, child-count=2} | +--->Toolbar{id=16909232, res-name=action_bar, visibility=VISIBLE, width=720, height=112, has-focus=false, has- focusable=false, 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=2} | +---->TextView{id=-1, visibility=VISIBLE, width=206, height=54, has-focus=false, has-focusable=false, 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=32.0, y=29.0, text=Hello World, input-type=0, ime-target=false, has-links=false}
  22. Screenshots on Failures Espresso.setFailureHandler(new FailureHandler() { @Override public void handle(Throwable

    throwable, Matcher<View> matcher) { ScreenshotHelper.takeScreenshotForcefully("test_failed"); try { new DefaultFailureHandler(applicationContext).handle(throwable, matcher); } catch (Exception e) { logger.error(e.getMessage(), e); throw new RuntimeException(e); } } });
  23. NO ONE Sees The Value of Your Tests… YET Your

    Bosses Product Owners Designers Manual Testers
  24. User Interfaces are VISUAL A User Interface is something very

    hard to represent in a non-visual way. Imagine you need to tell someone who is visually impaired what the Mona Lisa painting looks like.
  25. Spoon Test Runner Run instrumentation tests in parallel Shard tests

    across multiple devices Beautiful HTML reports java -jar spoon-runner-1.7.0-jar-with-dependencies.jar \
 --apk example-app.apk \
 --test-apk example-tests.apk
  26. How Taking Screenshots Work Traverse View Hierarchy Generate Image Save

    to disk in
 a specific location adb pull screenshot report finds images
  27. Should I use Devices or Emulators? Emulator for Development and

    Continuous Integration Use devices for release regressions
  28. Use The Android Emulator Run real versions of Android Configurable

    Faster than real devices Available on demand and scalable Cheaper and lower maintenance For screenshots, run low density (mdpi - 384x640)
  29. Combating Shared State Clearing preferences Clearing databases Clearing files Resetting

    singletons Resetting static variables Re-injecting Dependency Injections Killing any Background Work
  30. To Achieve This Run Test Dex Parser to dump all

    methods https://github.com/linkedin/dex-test-parser Execute each test method individually Combine test results CONS: Need custom scripts Can’t use Spoon Won’t work on Firebase Test Lab
  31. Android Test Orchestrator Announced at Google I/O 2017. Available in

    the upcoming version of the Android Testing Support Library. Runs "pm clear” before each run. Service APK running in the background. Will be integrated with Android Studio.
  32. Small, Targeted Tests Start in the activity you are testing

    with ActivityTestRule. If your login page breaks, none of your tests will pass. As with any test, isolating it and testing it with the expected inputs is the best way to understand if it’s passing.
  33. Skip to the important part Jump directly to what you

    are testing. Setup state without doing it through your UI.
  34. Hermetic and Deterministic Tests Mock everything external to what you

    are testing on the UI. Mock all API requests WireMock MockWebServer and RESTMock Mockito
  35. Continuous Integration Tests must be run to stay up to

    date Run on every PR Start with Local machine - Have to write and maintain scripts End with cloud hosted - Firebase Test Lab, Amazon Device Farm, Circle CI, Travis CI, BuddyBuild, etc.
  36. Speed - Parallelization & Sharding Sharding Infinite availability with cloud

    based services Shard 1 Shard 2 Shard 3 Shard 4 Shard 5 Run Tests