Expressive functional testing with Espresso
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
onSearchItemWithDoctor("Doctor House", R.id.search_doctor_calendar, ) check matches(isDisplayed())
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())
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 You && Q || A
Expressive functional testing with Espresso / Droidcon Stockholm 2015
By Maciej Górski
Expressive functional testing with Espresso / Droidcon Stockholm 2015
- 2,331