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