$30 off During Our Annual Pro Sale. View Details »

Taking care of your UI tests

Taking care of your UI tests

Most Android UI testing talks focus on how Espresso works and how to start writing simple tests.
This is fine for a simple app but maintaining and expanding those tests gets harder as the app grows. This talk will focus on writing complex tests scenarios while making sure we keep them maintainable, expandable and readable.

I’ll use a released app we developed as an example. This will involve multi-screen scenarios, handling asynchronous operations, providing a fake environment as well as some common and less common Espresso gotchas.

Talk given at DevFest Istanbul 2016, DevFest Berlin 2016 & Android Makers France 2017

Recording: https://www.youtube.com/watch?v=dcWTq7MyrBQ

Florian Mierzejewski

November 12, 2016
Tweet

More Decks by Florian Mierzejewski

Other Decks in Programming

Transcript

  1. TAKING CARE OF
    YOUR UI TESTS

    View Slide

  2. ESPRESSO

    View Slide

  3. Maintainable
    Extendable
    Readable
    Stable

    View Slide

  4. @florianmski

    View Slide

  5. View Slide

  6. View Slide

  7. PAGEOBJECT

    View Slide

  8. What?
    Wraps screen with an application-specific API
    Allow interacting with screen elements easily
    Test code not affected by UI changes

    View Slide

  9. Why?
    Test code easier to understand
    Logic is about the intention of the test
    Don’t care about UI details

    View Slide

  10. View Slide

  11. onView(withId(R.id.track_detail_actions_more))
    .perform(click());
    onView(withId(R.id.bottom_options_recycler_view))
    .perform(actionOnItem(hasDescendant(allOf(
    withId(R.id.bottom_options_item_title),
    withText(R.string.track_detail_action_private))),
    click()));
    onView(withId(android.R.id.button1)).perform(click());

    View Slide

  12. onView(withId(R.id.track_detail_actions_more))
    .perform(click());
    onView(withId(R.id.bottom_options_recycler_view))
    .perform(actionOnItem(hasDescendant(allOf(
    withId(R.id.bottom_options_item_title),
    withText(R.string.track_detail_action_private))),
    click()));
    onView(withId(android.R.id.button1)).perform(click());

    View Slide

  13. onView(withId(R.id.track_detail_actions_more))
    .perform(click());
    onView(withId(R.id.bottom_options_recycler_view))
    .perform(actionOnItem(hasDescendant(allOf(
    withId(R.id.bottom_options_item_title),
    withText(R.string.track_detail_action_private))),
    click()));
    onView(withId(android.R.id.button1)).perform(click());

    View Slide

  14. View Slide

  15. trackDetailScreen.showTrackOptions()
    .tapOnMakeTrackPrivateOption()
    .tapOnConfirmInDialog();

    View Slide

  16. View Slide

  17. SCREENS, INTERACTORS
    & ASSERTIONS

    View Slide

  18. Screens
    public class TrackDetailScreen extends Screen {


    private final TrackDetailInteractors interactors;


    public TrackDetailScreen() {

    super(R.id.track_detail_container);

    interactors = new TrackDetailInteractors();

    }


    public Options showTrackOptions() {

    interactors.trackActions().perform(click());

    return goTo(new Options());

    }
    }

    View Slide

  19. And part of screens…
    public static class Options extends BottomSheetScreen {


    public DialogScreen tapOnMakeTrackPrivateOption() {

    return tapOnOptionWithString(R.string.track_detail_action_private).goTo(dialog());

    }


    public DialogScreen tapOnMakeTrackPublicOption() {

    return tapOnOptionWithString(R.string.track_detail_action_public).goTo(dialog());

    }


    public UnknownScreen tapOnPlayTrackOption() {

    return tapOnOptionWithString(R.string.track_detail_action_play)
    .goTo(new UnknownScreen());

    }


    }

    View Slide

  20. And part of screens…
    public static class Options extends BottomSheetScreen {


    public DialogScreen tapOnMakeTrackPrivateOption() {

    return tapOnOptionWithString(R.string.track_detail_action_private).goTo(dialog());

    }


    public DialogScreen tapOnMakeTrackPublicOption() {

    return tapOnOptionWithString(R.string.track_detail_action_public).goTo(dialog());

    }


    public UnknownScreen tapOnPlayTrackOption() {

    return tapOnOptionWithString(R.string.track_detail_action_play)
    .goTo(new UnknownScreen());

    }


    }

    View Slide

  21. As well as dialogs…
    public class DialogScreen extends Screen {


    public Screen tapOnConfirmInDialog() {

    return tapOnButton(android.R.id.button1);

    }


    public Screen tapOnCancelInDialog() {

    return tapOnButton(android.R.id.button2);

    }


    protected Screen tapOnButton(@IdRes int buttonId) {

    onView(withId(buttonId)).perform(click());

    return previousScreen();

    }


    }

    View Slide

  22. Interactors
    public class TrackDetailInteractors {


    BetterViewInteraction trackActions() {

    return BetterViewInteraction.wrap(onView(withId(R.id.track_detail_actions_more)));

    }


    BetterViewInteraction privateIndicator() {

    return BetterViewInteraction.wrap(onView(withText(R.string.track_item_private)));

    }


    RecyclerViewScreen recyclerView() {

    return new RecyclerViewScreen(allOf(

    withId(R.id.ak_recycler_view),

    isDescendantOfA(withId(R.id.track_detail_container))));

    }

    }

    View Slide

  23. Assertions
    public class TrackDetailAssertions {


    private final TrackDetailInteractors interactors;


    private TrackDetailAssertions(TrackDetailInteractors interactors) {

    this.interactors = interactors;

    }


    public static TrackDetailAssertions assertTrackDetail() {

    return new TrackDetailAssertions(new TrackDetailInteractors());

    }


    public TrackDetailAssertions hasPrivacySetToPrivate() {

    interactors.privateIndicator()

    .check(matches(isDisplayed()), "Track is not displayed as being private.");

    return this;

    }
    }

    View Slide

  24. @Test

    public void shouldChangeTrackToPrivateInTrackDetails() {

    givenPublicTrackDetail();


    trackDetailScreen.showTrackOptions()

    .tapOnMakeTrackPrivateOption()

    .tapOnConfirmInDialog();


    assertTrackDetail().hasPrivacySetToPrivate();

    }

    View Slide

  25. @Test

    public void shouldChangeTrackToPrivateInTrackDetails() {

    givenPublicTrackDetail();


    trackDetailScreen.showTrackOptions()

    .tapOnMakeTrackPrivateOption()

    .tapOnConfirmInDialog();


    assertTrackDetail().hasPrivacySetToPrivate();

    }

    View Slide

  26. @Test

    public void shouldChangeTrackToPrivateInTrackDetails() {

    givenPublicTrackDetail();


    trackDetailScreen.showTrackOptions()

    .tapOnMakeTrackPrivateOption()

    .tapOnConfirmInDialog();


    assertTrackDetail().hasPrivacySetToPrivate();

    }

    View Slide

  27. View Slide

  28. INSTRUMENTATION
    TESTING ROBOTS
    https://realm.io/news/kau-jake-wharton-testing-robots/

    View Slide

  29. CONFIGURING

    View Slide

  30. Test classes
    Single screen
    Event propagation
    Navigation

    View Slide

  31. Test class “header”
    @Rule
    public final ActivityTestRule
    activityTestRule = new
    ActivityTestRule<>(UserPreviewActivity.class, true, false);
    @ClassRule
    public static final TestRule chain =
    RuleChain.outerRule(AccountTestRule.add(POPULAR_CREATOR))
    .around(applicationComponentTestRule())
    .around(new IdlingResourceTestRule(rxIdlingResource));

    View Slide

  32. Test class “header”
    @Rule
    public final ActivityTestRule
    activityTestRule = new
    ActivityTestRule<>(UserPreviewActivity.class, true, false);
    @ClassRule
    public static final TestRule chain =
    RuleChain.outerRule(AccountTestRule.add(POPULAR_CREATOR))
    .around(applicationComponentTestRule())
    .around(new IdlingResourceTestRule(rxIdlingResource));

    View Slide

  33. starting account rule
    starting application component rule
    starting idling resource rule
    execute all tests
    finished idling resource rule
    finished application component rule
    finished account rule

    View Slide

  34. starting account rule
    starting application component rule
    starting idling resource rule
    execute all tests
    finished idling resource rule
    finished application component rule
    finished account rule

    View Slide

  35. starting account rule
    starting application component rule
    starting idling resource rule
    execute all tests
    finished idling resource rule
    finished application component rule
    finished account rule

    View Slide

  36. starting account rule
    starting application component rule
    starting idling resource rule
    execute all tests
    finished idling resource rule
    finished application component rule
    finished account rule

    View Slide

  37. starting account rule
    starting application component rule
    starting idling resource rule
    execute all tests
    finished idling resource rule
    finished application component rule
    finished account rule

    View Slide

  38. starting account rule
    starting application component rule
    starting idling resource rule
    execute all tests
    finished idling resource rule
    finished application component rule
    finished account rule

    View Slide

  39. starting account rule
    starting application component rule
    starting idling resource rule
    execute all tests
    finished idling resource rule
    finished application component rule
    finished account rule

    View Slide

  40. View Slide

  41. LocaleTestRule
    @Override

    public Statement apply(final Statement base, Description description) {

    return new Statement() {

    @Override

    public void evaluate() throws Throwable {

    Locale defaultLocale = Locale.getDefault();

    for (Locale locale : locales) {

    changeLocaleTo(locale);

    base.evaluate();

    }

    changeLocaleTo(defaultLocale);

    }

    };

    }

    View Slide

  42. LocaleTestRule
    @Override

    public Statement apply(final Statement base, Description description) {

    return new Statement() {

    @Override

    public void evaluate() throws Throwable {

    Locale defaultLocale = Locale.getDefault();

    for (Locale locale : locales) {

    changeLocaleTo(locale);

    base.evaluate();

    }

    changeLocaleTo(defaultLocale);

    }

    };

    }

    View Slide

  43. LocaleTestRule
    @Override

    public Statement apply(final Statement base, Description description) {

    return new Statement() {

    @Override

    public void evaluate() throws Throwable {

    Locale defaultLocale = Locale.getDefault();

    for (Locale locale : locales) {

    changeLocaleTo(locale);

    base.evaluate();

    }

    changeLocaleTo(defaultLocale);

    }

    };

    }

    View Slide

  44. LocaleTestRule
    @Override

    public Statement apply(final Statement base, Description description) {

    return new Statement() {

    @Override

    public void evaluate() throws Throwable {

    Locale defaultLocale = Locale.getDefault();

    for (Locale locale : locales) {

    changeLocaleTo(locale);

    base.evaluate();

    }

    changeLocaleTo(defaultLocale);

    }

    };

    }

    View Slide

  45. AccountTestRule
    Add / Remove account from the AccountManager
    Test app with popular / normal / empty user
    Test Sign-in

    View Slide

  46. ApplicationComponentTestRule
    Swap dagger graph
    Disable crash logs / tracking
    Override components

    View Slide

  47. By default, Espresso waits for UI events in the current
    message queue to be handled and for default AsyncTasks
    to complete before it moves on to the next test operation.
    There are instances where applications perform
    background operations (such as communicating with web
    services) via non-standard means; for example: direct
    creation and management of threads.
    In such cases, you have to use Idling Resources to inform
    Espresso of the app’s long-running operations.

    View Slide

  48. By default, Espresso waits for UI events in the current
    message queue to be handled and for default AsyncTasks
    to complete before it moves on to the next test operation.
    There are instances where applications perform
    background operations (such as communicating with web
    services) via non-standard means; for example: direct
    creation and management of threads.
    In such cases, you have to use Idling Resources to inform
    Espresso of the app’s long-running operations.

    View Slide

  49. IdlingResourceTestRule
    Register / unregister idling resource before / after
    test(s)
    Used mostly in combination with RxIdlingResource

    View Slide

  50. public class ExecutionStrategy implements Observable.Transformer {


    private final Scheduler subscribeOnScheduler;

    private final Scheduler observeOnScheduler;


    public ExecutionStrategy(Scheduler subscribeOnScheduler, Scheduler
    observeOnScheduler) {

    this.subscribeOnScheduler = subscribeOnScheduler;

    this.observeOnScheduler = observeOnScheduler;

    }


    @Override

    public Observable call(Observable observable) {

    return observable

    .subscribeOn(subscribeOnScheduler)

    .observeOn(observeOnScheduler);

    }


    }

    View Slide

  51. public class ExecutionStrategy implements Observable.Transformer {


    private final Scheduler subscribeOnScheduler;

    private final Scheduler observeOnScheduler;


    public ExecutionStrategy(Scheduler subscribeOnScheduler, Scheduler
    observeOnScheduler) {

    this.subscribeOnScheduler = subscribeOnScheduler;

    this.observeOnScheduler = observeOnScheduler;

    }


    @Override

    public Observable call(Observable observable) {

    return observable

    .subscribeOn(subscribeOnScheduler)

    .observeOn(observeOnScheduler);

    }


    }

    View Slide

  52. RxIdlingResource
    public ExecutionStrategy wrap(ExecutionStrategy strategy) {


    return new ExecutionStrategy() {

    @Override

    public Observable call(Observable observable) {

    return wrap(executionStrategy.call(observable));

    }

    };

    }

    View Slide

  53. RxIdlingResource
    public ExecutionStrategy wrap(ExecutionStrategy strategy) {


    return new ExecutionStrategy() {

    @Override

    public Observable call(Observable observable) {

    return wrap(executionStrategy.call(observable));

    }

    };

    }

    View Slide

  54. RxIdlingResource
    public Observable wrap(final Observable observable) {

    return observable

    .doOnSubscribe(new Action0() {

    public void call() {

    isIdle.compareAndSet(true, false);

    }

    })

    .finallyDo(new Action0() {

    public void call() {

    if (isIdle.compareAndSet(false, true)) {

    if (resourceCallback != null) {

    resourceCallback.onTransitionToIdle();

    }

    }

    }

    });

    }

    View Slide

  55. public Observable wrap(final Observable observable) {

    return observable

    .doOnSubscribe(new Action0() {

    public void call() {

    isIdle.compareAndSet(true, false);

    }

    })

    .finallyDo(new Action0() {

    public void call() {

    if (isIdle.compareAndSet(false, true)) {

    if (resourceCallback != null) {

    resourceCallback.onTransitionToIdle();

    }

    }

    }

    });

    }
    RxIdlingResource

    View Slide

  56. public Observable wrap(final Observable observable) {

    return observable

    .doOnSubscribe(new Action0() {

    public void call() {

    isIdle.compareAndSet(true, false);

    }

    })

    .finallyDo(new Action0() {

    public void call() {

    if (isIdle.compareAndSet(false, true)) {

    if (resourceCallback != null) {

    resourceCallback.onTransitionToIdle();

    }

    }

    }

    });

    }
    RxIdlingResource

    View Slide

  57. Rules are good for you
    Reusable
    Unclutter your tests from unnecessary details
    Separate configuration and actual tests

    View Slide

  58. BETTER FAILURES

    View Slide

  59. Making ViewInteraction great again!
    Wrap ViewInteraction
    Use a FailureHandler to provide a human explanation
    of what went wrong

    View Slide

  60. Caused by: junit.framework.AssertionFailedError: 'is displayed
    on the screen to the user' doesn't match the selected view.
    Expected: is displayed on the screen to the user
    Got: "ab{id=2131689841, res-name=track_item_private_indicator,
    visibility=GONE, width=0, height=0, 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=true, is-selected=false, root-is-layout-
    requested=false, has-input-connection=false, x=0.0, y=0.0,
    text=Private, input-type=0, ime-target=false, has-links=false}”
    […]

    View Slide

  61. public BetterViewInteraction check(ViewAssertion viewAssert,
    String failureMessage,
    Object... args) {

    viewInteraction.withFailureHandler(new FailureHandler() {

    @Override

    public void handle(Throwable t, Matcher viewMatcher) {
    String errorMessage = String.format(failureMessage, args);

    Throwable error = new AssertionFailed(errorMessage, t);

    defaultFailureHandler.handle(error, viewMatcher);

    }

    }).check(viewAssert);

    return this;

    }

    View Slide

  62. public BetterViewInteraction check(ViewAssertion viewAssert,
    String failureMessage,
    Object... args) {

    viewInteraction.withFailureHandler(new FailureHandler() {

    @Override

    public void handle(Throwable t, Matcher viewMatcher) {
    String errorMessage = String.format(failureMessage, args);

    Throwable error = new AssertionFailed(errorMessage, t);

    defaultFailureHandler.handle(error, viewMatcher);

    }

    }).check(viewAssert);

    return this;

    }

    View Slide

  63. public BetterViewInteraction check(ViewAssertion viewAssert,
    String failureMessage,
    Object... args) {

    viewInteraction.withFailureHandler(new FailureHandler() {

    @Override

    public void handle(Throwable t, Matcher viewMatcher) {
    String errorMessage = String.format(failureMessage, args);

    Throwable error = new AssertionFailed(errorMessage, t);

    defaultFailureHandler.handle(error, viewMatcher);

    }

    }).check(viewAssert);

    return this;

    }

    View Slide

  64. com.soundcloud.espresso.EspressoAssertionFailedError: Track is not displayed
    as being private.
    […]
    Caused by: junit.framework.AssertionFailedError: 'is displayed on the screen
    to the user' doesn't match the selected view.
    Expected: is displayed on the screen to the user
    Got: "ab{id=2131689841, res-name=track_item_private_indicator,
    visibility=GONE, width=0, height=0, 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=true, is-selected=false, root-is-
    layout-requested=false, has-input-connection=false, x=0.0, y=0.0,
    text=Private, input-type=0, ime-target=false, has-links=false}”
    […]

    View Slide

  65. GOING DEEPER

    View Slide

  66. Feature flags
    Based on the flavour
    Developing continuously
    Switching a flag when we want to release the feature

    View Slide

  67. GREAT BUT… HOW DO
    WE DEAL WITH TESTING
    BOTH NEW & OLD
    BEHAVIOUR?

    View Slide

  68. A CUSTOM TEST RUNNER!
    OR, HOW TO SKIP TESTS
    BASED ON A PREDICATE

    View Slide

  69. @RunWhen(
    value = SomePredicate.class,
    expected = false)


    View Slide

  70. public final class SomeFeatureFlagEnabled extends Predicate {

    @Override

    public boolean evaluate() {

    return featureFlags().someFeature();

    }


    @Override

    public String description() {

    return “Some feature flag enabled";

    }

    }

    View Slide

  71. public class SkippableAndroidJUnit4 extends BlockJUnit4ClassRunner {


    @Override

    protected Statement methodBlock(FrameworkMethod testMethod) {

    Statement base = super.methodBlock(testMethod);

    Statement statement = applyRunWhenTo(testMethod.getMethod(), base);


    Class> clazz = testMethod.getDeclaringClass();

    do {

    statement = applyRunWhenTo(clazz, statement);

    } while ((clazz = clazz.getSuperclass()) != null);

    return statement;

    }


    private Statement applyRunWhenTo(AnnotatedElement element, final Statement base) {

    RunWhen annotation = element.getAnnotation(RunWhen.class);

    if (annotation == null) {

    return base;

    }

    return new PredicateStatement(base, annotation.value(), annotation.expected());

    }


    }

    View Slide

  72. public class SkippableAndroidJUnit4 extends BlockJUnit4ClassRunner {


    @Override

    protected Statement methodBlock(FrameworkMethod testMethod) {

    Statement base = super.methodBlock(testMethod);

    Statement statement = applyRunWhenTo(testMethod.getMethod(), base);


    Class> clazz = testMethod.getDeclaringClass();

    do {

    statement = applyRunWhenTo(clazz, statement);

    } while ((clazz = clazz.getSuperclass()) != null);

    return statement;

    }


    private Statement applyRunWhenTo(AnnotatedElement element, final Statement base) {

    RunWhen annotation = element.getAnnotation(RunWhen.class);

    if (annotation == null) {

    return base;

    }

    return new PredicateStatement(base, annotation.value(), annotation.expected());

    }


    }

    View Slide

  73. class PredicateStatement extends Statement {

    private final Statement base;

    private final Class extends Predicate> predicateClass;

    private final Boolean expected;


    public PredicateStatement(Statement base, Class extends Predicate>
    predicateClass, Boolean expected) {

    this.base = base;

    this.predicateClass = predicateClass;

    this.expected = expected;

    }


    @Override

    public void evaluate() throws Throwable {

    Predicate predicate = PredicateBuilder.build(predicateClass);

    assumeThat(predicate.description(), predicate.evaluate(),
    Matchers.is(expected));

    base.evaluate();

    }

    }

    View Slide

  74. class PredicateStatement extends Statement {

    private final Statement base;

    private final Class extends Predicate> predicateClass;

    private final Boolean expected;


    public PredicateStatement(Statement base, Class extends Predicate>
    predicateClass, Boolean expected) {

    this.base = base;

    this.predicateClass = predicateClass;

    this.expected = expected;

    }


    @Override

    public void evaluate() throws Throwable {

    Predicate predicate = PredicateBuilder.build(predicateClass);

    assumeThat(predicate.description(), predicate.evaluate(),
    Matchers.is(expected));

    base.evaluate();

    }

    }

    View Slide

  75. GRADLE

    View Slide

  76. def unlockAll = project.task('unlockAllDevices')


    command.devices().eachWithIndex { device, index ->


    def unlocker = project.task("unlockDevice${index}", type: ForceDeviceUnlock) {

    deviceId = device.id

    }

    unlockAll.dependsOn unlocker

    }


    project.afterEvaluate {

    project.tasks.findAll {

    it.name.startsWith('connected') && it.name.endsWith('AndroidTest')

    }*.dependsOn(unlockAll)

    }

    View Slide

  77. def unlockAll = project.task('unlockAllDevices')


    command.devices().eachWithIndex { device, index ->


    def unlocker = project.task("unlockDevice${index}", type: ForceDeviceUnlock) {

    deviceId = device.id

    }

    unlockAll.dependsOn unlocker

    }


    project.afterEvaluate {

    project.tasks.findAll {

    it.name.startsWith('connected') && it.name.endsWith('AndroidTest')

    }*.dependsOn(unlockAll)

    }

    View Slide

  78. class ForceDeviceUnlock extends AdbTask {


    @TaskAction

    void unlockDevice() {

    println 'Trying to unlock device using UnlockHelperApp (if installed)'

    def startHelperAppCommand = ['shell', 'am', 'start', '-n', 'io.appium.unlock/.Unlock']

    assertDeviceAndRunCommand(startHelperAppCommand)


    println 'Trying to unlock device using unlock keycode'

    def pressUnlockKey = ['shell', 'input', 'keyevent', '82']

    assertDeviceAndRunCommand(pressUnlockKey)


    println 'Sending "home" keycode to reset the screen'

    def pressHomeKey = ['shell', 'input', 'keyevent', '3']

    assertDeviceAndRunCommand(pressHomeKey)

    }

    }

    View Slide

  79. class ForceDeviceUnlock extends AdbTask {


    @TaskAction

    void unlockDevice() {

    println 'Trying to unlock device using UnlockHelperApp (if installed)'

    def startHelperAppCommand = ['shell', 'am', 'start', '-n', 'io.appium.unlock/.Unlock']

    assertDeviceAndRunCommand(startHelperAppCommand)


    println 'Trying to unlock device using unlock keycode'

    def pressUnlockKey = ['shell', 'input', 'keyevent', '82']

    assertDeviceAndRunCommand(pressUnlockKey)


    println 'Sending "home" keycode to reset the screen'

    def pressHomeKey = ['shell', 'input', 'keyevent', '3']

    assertDeviceAndRunCommand(pressHomeKey)

    }

    }

    View Slide

  80. class ForceDeviceUnlock extends AdbTask {


    @TaskAction

    void unlockDevice() {

    println 'Trying to unlock device using UnlockHelperApp (if installed)'

    def startHelperAppCommand = ['shell', 'am', 'start', '-n', 'io.appium.unlock/.Unlock']

    assertDeviceAndRunCommand(startHelperAppCommand)


    println 'Trying to unlock device using unlock keycode'

    def pressUnlockKey = ['shell', 'input', 'keyevent', '82']

    assertDeviceAndRunCommand(pressUnlockKey)


    println 'Sending "home" keycode to reset the screen'

    def pressHomeKey = ['shell', 'input', 'keyevent', '3']

    assertDeviceAndRunCommand(pressHomeKey)

    }

    }

    View Slide

  81. class ForceDeviceUnlock extends AdbTask {


    @TaskAction

    void unlockDevice() {

    println 'Trying to unlock device using UnlockHelperApp (if installed)'

    def startHelperAppCommand = ['shell', 'am', 'start', '-n', 'io.appium.unlock/.Unlock']

    assertDeviceAndRunCommand(startHelperAppCommand)


    println 'Trying to unlock device using unlock keycode'

    def pressUnlockKey = ['shell', 'input', 'keyevent', '82']

    assertDeviceAndRunCommand(pressUnlockKey)


    println 'Sending "home" keycode to reset the screen'

    def pressHomeKey = ['shell', 'input', 'keyevent', '3']

    assertDeviceAndRunCommand(pressHomeKey)

    }

    }

    View Slide

  82. DEVICE GOTCHAS

    View Slide

  83. “Ok Google”
    Autocomplete
    Updating ADB
    Update Dialog
    ☀ &

    View Slide

  84. View Slide

  85. QUESTIONS?
    @florianmski

    View Slide