The Real Face

functional testing

of

Meet my PO

Espresso

Espresso

Espresso

void testShouldShowPasswordTooShort() {
  onView withId(R.id.login_email) \
      perform typeText('correct@email.com')
  onView withId(R.id.login_password) \
      perform typeText('aaa')
  onView withId(R.id.login_submit) \
      perform click()
  onView withId(R.id.login_error_message) \
      check matches(withText('Password too short.'))
}

Espresso

void testShouldShowPasswordTooShort() {
  onView withId(R.id.login_email) \
      perform typeText('correct@email.com')
  onView withId(R.id.login_password) \
      perform typeText('aaa')
  onView withId(R.id.login_submit) \
      perform click()
  onView withId(R.id.login_error_message) \
      check matches(withText('Password too short.'))
}

Espresso

void testShouldShowPasswordTooShort() {
  onView withId(R.id.login_email) \
      perform typeText('correct@email.com')
  onView withId(R.id.login_password) \
      perform typeText('aaa')
  onView withId(R.id.login_submit) \
      perform click()
  onView withId(R.id.login_error_message) \
      check matches(withText('Password too short.'))
}

Espresso

void testShouldShowPasswordTooShort() {
  onView withId(R.id.login_email) \
      perform typeText('correct@email.com')
  onView withId(R.id.login_password) \
      perform typeText('aaa')
  onView withId(R.id.login_submit) \
      perform click()
  onView withId(R.id.login_error_message) \
      check matches(withText('Password too short.'))
}

Espresso

void testShouldShowPasswordTooShort() {
  onView withId(R.id.login_email) \
      perform typeText('correct@email.com')
  onView withId(R.id.login_password) \
      perform typeText('aaa')
  onView withId(R.id.login_submit) \
      perform click()
  onView withId(R.id.login_error_message) \
      check matches(withText('Password too short.'))
}

Espresso

void testShouldShowPasswordTooShort() {
  onView withId(R.id.login_email) \
      perform typeText('correct@email.com')
  onView withId(R.id.login_password) \
      perform typeText('aaa')
  onView withId(R.id.login_submit) \
      perform click()
  onView withId(R.id.login_error_message) \
      check matches(withText('Password too short.'))
}

Espresso

void testShouldShowPasswordTooShort() {
  onView withId(R.id.login_email) \
      perform typeText('correct@email.com')
  onView withId(R.id.login_password) \
      perform typeText('aaa')
  onView withId(R.id.login_submit) \
      perform click()
  onView withId(R.id.login_error_message) \
      check matches(withText('Password too short.'))
}

Espresso

void testShouldShowPasswordTooShort() {
  onView withId(R.id.login_email) \
      perform typeText('correct@email.com')
  onView withId(R.id.login_password) \
      perform typeText('aaa')
  onView withId(R.id.login_submit) \
      perform click()
  onView withId(R.id.login_error_message) \
      check matches(withText('Password too short.'))
}

Abstraction

void testShouldShowPasswordTooShort() {
  onView withId(R.id.login_email) \
      perform typeText('correct@email.com')
  onView withId(R.id.login_password) \
      perform typeText('aaa')
  onView withId(R.id.login_submit) \
      perform click()
  onView withId(R.id.login_error_message) \
      check matches(withText('Password too short.'))
}

Abstraction

void testShouldShowPasswordTooShort() {
  onView withId(R.id.login_email) \
      perform typeText('correct@email.com')
  onView withId(R.id.login_password) \
      perform typeText('aaa')
  onView withId(R.id.login_submit) \
      perform click()
  onView withId(R.id.login_error_message) \
      check matches(withText('Password too short.'))
}

Abstraction

void testShouldShowPasswordTooShort() {
  login('correct@email.com', 'aaa')
  onView withId(R.id.login_error_message) \
      check matches(withText('Password too short.'))
}

Abstraction

void testShouldShowPasswordTooShort() {
  login('correct@email.com', 'aaa')
  onView withId(R.id.login_error_message) \
      check matches(withText('Password too short.'))
}

Abstraction

void testShouldShowPasswordTooShort() {
  login('correct@email.com', 'aaa')
  checkLoginErrorMessage('Password too short.')
}

Abstraction

void testShouldShowPasswordTooShort() {
  login('correct@email.com', 'aaa')
  checkLoginErrorMessage('Password too short.')
}

Abstraction

void testShouldShowPasswordTooShort() {
  login('correct@email.com', 'aaa')
  checkLoginErrorMessage('Password too short.')
}

Abstraction

static void login(String email, String password) {
  typeText(R.id.login_email, email)
  typeText(R.id.login_password, password)
  click(R.id.login_submit)
}

Abstraction

static void login(String email, String password) {
  typeText(R.id.login_email, email)
  typeText(R.id.login_password, password)
  click(R.id.login_submit)
}

Abstraction

static void typeText(@IdRes int id, String text) {
  onView withId(id) perform typeText(text)
}

Abstraction

void testShouldShowPasswordTooShort() {
  login('correct@email.com', 'aaa')
  checkLoginErrorMessage('Password too short.')
}

But my app...

uses Internets

But my app...

subscription = service.byLocation(q, lat, lon)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(this.&showDocs, this.&handleError)

But my app...

final class ResultsActivity extends BaseActivity {

  @Inject
  protected SearchService service
  private Subscription subscription

  // ...
}

But my app...

@Provides
SearchService provideSearchService() {
  return { String q, double lat, double lon ->
    // ...
    return Observable.just(list)
  } as SearchService
}

But my app...

uses GPS

But my app...

interface LocationService {

    Observable<Location> getCurrentLocation()
}

But my app...

@Provides
LocationService provideLocationService() {
  return {
    return just(createLocation(52.225, 21.01))
  } as LocationService
}

But my app...

uses Facebook

login button

But my app...

final class LoginActivity extends BaseActivity {

  @Inject
  protected FacebookLoginButtonProvider provider

  // ...
}

But my app...

interface FacebookLoginButtonProvider {

  void addToContainer(ViewGroup container,
      SuccessCallback successCallback,
      ErrorCallback errorCallback)

  // ...
}

But my app...

def button = new Button(container.context)
button.id = R.id.facebook_login_button
button.onClickListener = {
  successCallback.onSuccess("facebook_token")
}
container.addView(button)

But my app...

def button = new Button(container.context)
button.id = R.id.facebook_login_button
button.onClickListener = {
  errorCallback.onError(new RuntimeException())
}
container.addView(button)

Talk is cheap,

show me

the tests

WAT

@Override
protected void tearDown() throws Exception {
  super.tearDown()
  pressBackSeveralTimes()
}

WAT

private static void pressBackSeveralTimes() {
  try {
    6.times { pressBack() }
  } catch (NoActivityResumedException ignore) {
  }
}

WAT

final class SleepyCloseKeyboard implements ViewAction {
  ViewAction original = new CloseKeyboardAction()
  @Override
  void perform(UiController uiController, View view) {
    original.perform(uiController, view)
    if (BuildConfig.BUILT_ON_TRAVIS) {
      uiController.loopMainThreadForAtLeast(2_000)
    }
  }
}

Missing stuff

static Matcher<View> hasImage(@DrawableRes int id) {
  return new TypeSafeMatcher<ImageView>() {
    @Override
    boolean matchesSafely(ImageView imageView) {
      // ...
      return state == drawable.constantState
    }
    // ...
  }
}

Missing stuff

onView(allOf(
   isDescendantOfA(allOf(
      withParent(withId(R.id.search_list)),
      hasDescendant(allOf(
         withId(R.id.search_doctor_name),
         withText("Doctor House"))
      )
   )),
   withId(R.id.search_doctor_calendar))
) check matches(isDisplayed())

Missing stuff

onSearchItemWithDoctor("Doctor House",
    R.id.search_doctor_calendar,
) check matches(isDisplayed())

Wrap up

Wrap up

  • Expressiveness of Espresso (in Groovy)

Wrap up

  • Expressiveness of Espresso (in Groovy)
  • Abstraction layer (or two) in tests

Wrap up

  • Expressiveness of Espresso (in Groovy)
  • Abstraction layer (or two) in tests
  • Dependency Injection helps

Wrap up

  • Expressiveness of Espresso (in Groovy)
  • Abstraction layer (or two) in tests
  • Dependency Injection helps
  • DI helps even with UI

Wrap up

  • Expressiveness of Espresso (in Groovy)
  • Abstraction layer (or two) in tests
  • Dependency Injection helps
  • DI helps even with UI
  • It's not all wine and roses

Thank my PO

@cfksz

Thank You && Q || A

The Real Face of Functional Testing / Droidcon Berlin 2015

By Maciej Górski

The Real Face of Functional Testing / Droidcon Berlin 2015

  • 2,388