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,468