Slide 1

Slide 1 text

MAINTAINABLE ESPRESSO TESTS WITH ROBOTS AND SCREENSHOTS SAM EDWARDS - @HANDSTANDSAM

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Audience Poll: Testing? Espresso? Screenshots? Continuous Integration?

Slide 4

Slide 4 text

Why Espresso? Created by Google Access to app internals Easy to write Blazing fast

Slide 5

Slide 5 text

Using Espresso

Slide 6

Slide 6 text

Using Espresso

Slide 7

Slide 7 text

Using Espresso

Slide 8

Slide 8 text

Using Espresso

Slide 9

Slide 9 text

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()); }

Slide 10

Slide 10 text

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()); }

Slide 11

Slide 11 text

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()); }

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

Espresso Test Recorder (Android Studio 2.2+)

Slide 14

Slide 14 text

Espresso Test Recorder (Android Studio 2.2+)

Slide 15

Slide 15 text

Espresso Test Recorder (Android Studio 2.2+)

Slide 16

Slide 16 text

Espresso Test Recorder (Android Studio 2.2+)

Slide 17

Slide 17 text

Layout Inspector

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

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.”

Slide 20

Slide 20 text

When Should I write an Espresso test? Avoid unless necessary Only testing the UI Small and targeted

Slide 21

Slide 21 text

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);

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

Configuration for Success Disabled Animations Use Idling Resources for Background work Utilize AsyncTask Thread pool

Slide 27

Slide 27 text

Where can I learn more?

Slide 28

Slide 28 text

ROBOT TESTING PATTERN FOR ANDROID INSTRUMENTATION TESTS TEST TEST TEST TEST TEST

Slide 29

Slide 29 text

ROBOT TESTING PATTERN FOR ANDROID INSTRUMENTATION TESTS TEST TEST TEST TEST TEST

Slide 30

Slide 30 text

ROBOT TESTING PATTERN FOR ANDROID INSTRUMENTATION TESTS TEST TEST TEST TEST TEST X X X X X

Slide 31

Slide 31 text

ROBOT TESTING PATTERN FOR ANDROID INSTRUMENTATION TESTS TEST TEST TEST TEST TEST ROBOT

Slide 32

Slide 32 text

ROBOT TESTING PATTERN FOR ANDROID INSTRUMENTATION TESTS TEST TEST TEST TEST TEST ROBOT X

Slide 33

Slide 33 text

ROBOT TESTING PATTERN FOR ANDROID INSTRUMENTATION TESTS TEST TEST TEST TEST TEST ROBOT WHAT HOW

Slide 34

Slide 34 text

LOGIN ROBOT new LoginRobot() .username("sam") .password("password") .toggleRememberMe() .login();

Slide 35

Slide 35 text

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; } }

Slide 36

Slide 36 text

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; } }

Slide 37

Slide 37 text

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; } }

Slide 38

Slide 38 text

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; } }

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

Screenshots - WHY? 1. See What’s Being Tested

Slide 47

Slide 47 text

1. See What’s Being Tested

Slide 48

Slide 48 text

1. See What’s Being Tested

Slide 49

Slide 49 text

1. See What’s Being Tested

Slide 50

Slide 50 text

1. See What’s Being Tested

Slide 51

Slide 51 text

Screenshots - WHY? 1. See What’s Being Tested 2. Diagnose Failures

Slide 52

Slide 52 text

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}

Slide 53

Slide 53 text

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}

Slide 54

Slide 54 text

Screenshots on Failures

Slide 55

Slide 55 text

Screenshots on Failures Espresso.setFailureHandler(new FailureHandler() { @Override public void handle(Throwable throwable, Matcher matcher) { ScreenshotHelper.takeScreenshotForcefully("test_failed"); try { new DefaultFailureHandler(applicationContext).handle(throwable, matcher); } catch (Exception e) { logger.error(e.getMessage(), e); throw new RuntimeException(e); } } });

Slide 56

Slide 56 text

Screenshots - WHY? 1. See What’s Being Tested 2. Diagnose Failures 3. Share Your Tests

Slide 57

Slide 57 text

NO ONE Sees The Value of Your Tests… YET Your Bosses Product Owners Designers Manual Testers

Slide 58

Slide 58 text

To them it’s just console output.

Slide 59

Slide 59 text

Let EVERYONE see
 what’s being tested.

Slide 60

Slide 60 text

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.

Slide 61

Slide 61 text

Same Test, But Looks Different on Different Devices

Slide 62

Slide 62 text

DEVICE SIZE OS VERSION DEVICE MODEL LANGUAGE

Slide 63

Slide 63 text

Test Reports Spoon Screenshot Reports Isolated Logs for each test execution

Slide 64

Slide 64 text

Spoon http://square.github.io/spoon/

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

Spoon HTML Reports

Slide 67

Slide 67 text

No content

Slide 68

Slide 68 text

How Taking Screenshots Work Traverse View Hierarchy Generate Image Save to disk in
 a specific location adb pull screenshot report finds images

Slide 69

Slide 69 text

Screenshot Libraries Spoon Falcon UiAutomator

Slide 70

Slide 70 text

Other Screenshot Libraries Firebase Test Lab - Screen Shotter Fastlane - Screengrab

Slide 71

Slide 71 text

Should I use Devices or Emulators? Emulator for Development and Continuous Integration Use devices for release regressions

Slide 72

Slide 72 text

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)

Slide 73

Slide 73 text

No content

Slide 74

Slide 74 text

Combating Shared State Clearing preferences Clearing databases Clearing files Resetting singletons Resetting static variables Re-injecting Dependency Injections Killing any Background Work

Slide 75

Slide 75 text

How AndroidJUnitRunner Works The more you know…

Slide 76

Slide 76 text

No content

Slide 77

Slide 77 text

No content

Slide 78

Slide 78 text

No content

Slide 79

Slide 79 text

Isolated Test Execution Run each test completely separated adb shell pm clear com.mypackage (Total data wipe)

Slide 80

Slide 80 text

No content

Slide 81

Slide 81 text

No content

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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.

Slide 84

Slide 84 text

No content

Slide 85

Slide 85 text

No content

Slide 86

Slide 86 text

No content

Slide 87

Slide 87 text

No content

Slide 88

Slide 88 text

No content

Slide 89

Slide 89 text

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.

Slide 90

Slide 90 text

Skip to the important part Jump directly to what you are testing. Setup state without doing it through your UI.

Slide 91

Slide 91 text

Hermetic and Deterministic Tests Mock everything external to what you are testing on the UI. Mock all API requests WireMock MockWebServer and RESTMock Mockito

Slide 92

Slide 92 text

No content

Slide 93

Slide 93 text

No content

Slide 94

Slide 94 text

Intent Boundaries for Targeted Tests

Slide 95

Slide 95 text

No content

Slide 96

Slide 96 text

No content

Slide 97

Slide 97 text

No content

Slide 98

Slide 98 text

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.

Slide 99

Slide 99 text

Speed - Parallelization & Sharding Sharding Infinite availability with cloud based services Shard 1 Shard 2 Shard 3 Shard 4 Shard 5 Run Tests

Slide 100

Slide 100 text

Words of Wisdom

Slide 101

Slide 101 text

WITH ROBOTS AND SCREENSHOTS MAINTAINABLE ESPRESSO TESTS THANK YOU! SAM EDWARDS - @HANDSTANDSAM