Expressive functional testing with Espresso

Meet my PO

Espresso

Espresso

Espresso

public void testShouldShowPasswordShortErrorMessage() {
  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 testShouldShowPasswordShortErrorMessage() {
  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 testShouldShowPasswordShortErrorMessage() {
  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 testShouldShowPasswordShortErrorMessage() {
  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 testShouldShowPasswordShortErrorMessage() {
  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 testShouldShowPasswordShortErrorMessage() {
  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 testShouldShowPasswordShortErrorMessage() {
  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 testShouldShowPasswordShortErrorMessage() {
  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 testShouldShowPasswordShortErrorMessage() {
  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 testShouldShowPasswordShortErrorMessage() {
  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 testShouldShowPasswordShortErrorMessage() {
  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 testShouldShowPasswordShortErrorMessage() {
  login('correct@email.com', 'aaa')
  onView withId(R.id.login_error_message) \
      check matches(withText('Password too short.'))
}

Abstraction

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

Abstraction

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

Abstraction

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

Abstraction

void testShouldShowPasswordShortErrorMessage() {
  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 viewId, String text) {
  onView withId(viewId) perform typeText(text)
}

Abstraction

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

But my app... uses Internets

But my app...

subscription = service.doctorsByLocation(query, location)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(this.&showSearchResults, this.&handleError)

But my app...

final class SearchResultsActivity extends BaseActivity {

  @Inject
  protected SearchService service
  private Subscription subscription

  // ...
}

But my app...

final class SearchByLocationApiStubFactory {

  static SearchByLocationApi withCorrectData() {
    return { String query, double lat, double lon ->
      def firstDoctor = DoctorFactory.withCalendar()
      // ...
      return Observable.just(wrapper)
    } as SearchByLocationApi
  }
}

But my app...

@Module(overrides = true)
public final class HappyFlowTestModule {

  // ...

  @Provides
  @Singleton
  SearchByLocationApi provideSearchByLocationApi() {
    return SearchByLocationApiStubFactory.withCorrectData();
  }
}

But my app... uses GPS

But my app...

interface LocationService {

    Location getLastKnownLocation()

    Observable<Location> getCurrentLocation()
}

But my app...

@Provides
@Singleton
LocationService provideLocationService() {
  return new LocationService() {
    // ...
    @Override
    public Observable<Location> getCurrentLocation() {
      return just(LocationUtils.create(45.8, 15.9));
    }
  };
}

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

WAT

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

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

WAT

com.docplanner.functional.search
.googleplaces.SearchPlaceTestCase > testShouldUpdateLocationTextAfterChoosingLocation
FromResults[test(AVD) - 4.1.2] FAILED
    android.support.test.espresso.PerformException: Error performing 'single click' on view 'with id: pl.znanylekarz.debug:id/search_autocomplete_list'.
    at android.support.test.espresso.PerformException
$Builder.build(PerformException.java:83)
:app:connectedAndroidTestPolishDebug FAILED

WAT

final class SleepyCloseKeyboard implements ViewAction {

  ViewAction original = new CloseKeyboardAction()

  @Override
  void perform(UiController uiController, View view) {
    original.perform(uiController, view)
    if (System.getenv('TRAVIS')) {
      uiController.loopMainThreadForAtLeast(2_000)
    }
  }
}

Missing stuff

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

    // ...
  }
}

Missing stuff

onAutocompleteItemWithDoctor("Doctor House",
    R.id.search_autocomplete_doctor_calendar,
) check matches(isDisplayed())

Missing stuff

onView(allOf(
   isDescendantOfA(allOf(
      withParent(withId(R.id.search_autocomplete_list)),
      hasDescendant(allOf(
         withId(R.id.search_autocomplete_doctor_name),
         withText("Doctor House"))
      )
   )),
   withId(R.id.search_autocomplete_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
  • Dependency Injection helps

Wrap up

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

Wrap up

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

Wrap up

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

Thank my PO

@cfksz

Thank You && Q || A

Expressive functional testing with Espresso / DroidCon Zagreb 2015

By Maciej Górski

Expressive functional testing with Espresso / DroidCon Zagreb 2015

  • 2,397