A guide to automated
Android testing
by @darrillaga
Challenges
What to test
1. Utilities
2. Android components
3. UI
4. Expected user behavior
What to test
1. Utilities
2. Android components
3. UI
4. Expected user behavior
Utilities
Utilities are every piece of code, both pure java
and Android-dependant, that are used as tools
in the development process.
What to test
1. Utilities
2. Android components
3. UI
4. Expected user behavior
Android components
1. Activities
2. Fragments
3. Services
These are the most common components.
What to test
1. Utilities
2. Android components
3. UI
4. Expected user behavior
UI
UI specific behavior and custom views.
What to test
1. Utilities
2. Android components
3. UI
4. Expected user behavior
Expected user behavior
Functional testing on our activities, based on
expected behavior.
How to test
1. Using support libraries
2. Stub code we can’t control while testing
3. Decoupled code
4. Testing everything that could be error-prone
How to test
1. Using support libraries
2. Stub code we can’t control while testing
3. Decoupled code
4. Testing everything that could be error-prone
Using support libraries
1. Instrumentation
2. Android JUnit Runner
3. Espresso
4. UIAutomator
Using support libraries
1. Instrumentation
2. Android JUnit Runner
3. Espresso
4. UIAutomator
Instrumentation
This framework monitors all of the interaction
between the Android system and the
application.
Android testing libraries are built on top of the
Instrumentation framework.
Using support libraries
1. Instrumentation
2. Android JUnit Runner
3. Espresso
4. UIAutomator
AndroidJUnitRunner
JUnit test runner that lets you run JUnit 3 or JUnit 4-style
test classes on Android devices, including those using the
Espresso and UI Automator testing frameworks.
The test runner handles loading your test package and the
app under test to a device, running your tests, and
reporting test results.
Using support libraries
1. Instrumentation
2. Android JUnit Runner
3. Espresso
4. UIAutomator
Espresso
APIs for writing UI tests to simulate user interactions within
a single target app.
Provides automatic sync of test actions with the app’s UI.
Using support libraries
1. Instrumentation
2. Android JUnit Runner
3. Espresso
4. UIAutomator
UI Automator
This framework provides a set of APIs to build UI tests that
perform interactions on user apps and system apps.
The UI Automator APIs allows you to perform operations
such as opening the Settings menu or the app launcher in a
test device.
How to test
1. Using support libraries
2. Stub code we can’t control while testing
3. Decoupled code
4. Testing everything that could be error-prone
Stub code we can’t control with testing
1. Robospice
2. Card reader
3. Thermal printer SDK
4. Bluetooth
How to test
1. Using support libraries
2. Stub code we can’t control while testing
3. Decoupled code
4. Testing everything that could be error-prone
Decoupled code
1. Android components only in charge of
lifecycle
2. Layer for business logic
3. Layer modeling views
4. Create utilities for code that can be unit
tested
How to test
1. Using support libraries
2. Stub code we can’t control while testing
3. Decoupled code
4. Testing everything that could be error-prone
Testing everything that could be error-prone
1. Do not test getter/setters
2. Test code created by us
3. Do not test third-party code (stub it or let it
be)
Wrapping up
Android
Components
Utilities
Android system
UI
Stubs
Android
Components
Utilities
Android system
UI
Stubs
Android
Components
Utilities
Android system
UI
Stubs
Unit testing
● jUnit
● Instrumentation
jUnit
Unit test
private EmailValidator subject = new EmailValidator();
@Test
public void doesNotValidateWithInvalidEmail() {
String email = "invalidEmail";
assertThat(subject.isValid(email), is(false));
}
jUnit + Instrumentation
Unit test
// This component uses the Android framework out of our control so we need to use instrumentation to provide a context
private CurrentUserSessionService subject = new CurrentUserSessionService(InstrumentationRegistry.
getTargetContext());
@Test
public void isCurrentSessionPresentReturnsTrueWithCurrentUser() {
subject.saveUserSession(buildUserSession());
assertThat(subject.isCurrentSessionPresent(), is(true));
}
private UserSession buildUserSession() {
// creates stub user session instance
}
Android
Components
Utilities
Android system
UI
Stubs
Android
Components
Utilities
Android system
UI
Stubs
Using manual or library-assisted dependency injection to
switch dependencies for stubs when testing
Android
Components
Utilities
Android system
UI
Stubs
Android
Components
Utilities
Android system
UI
Stubs
Espresso to manipulate and unit-
test views in an isolated Activity
Android
Components
Utilities
Android system
UI
Stubs
Android
Components
Utilities
Android system
UI
Stubs
UI automator
Functional test of interactions
between the app and the Android
system
Android
Components
Utilities
Android system
UI
Stubs
Functional testing
● Espresso
● Espresso Intents
● Instrumentation
● Stubs
Functional test
@Test
public void unauthorizedUserLogin() {
registerInvalidCredentialsLoginRequest();
setUserAndPassword();
clickOnSignIn();
Context context = InstrumentationRegistry.getTargetContext();
String errorMessage = context.getString(R.string.InvalidCredentialsError);
onView(allOf(withText(errorMessage), withParent(withId(R.id.errors_wrapper)))).
check(matches(isDisplayed()));
}
LoginActivity - Android components + Stubs + Espresso + Instrumentation
Stub
LoginActivity
@Rule
public SpicedTestRule<LoginActivity> mTestRule = new SpicedTestRule<>(LoginActivity.class);
private void registerInvalidCredentialsLoginRequest() {
Charset utf8 = Charset.forName("utf-8");
mTestRule.registerResponseFor(
LoginRequest.class,
new HttpClientErrorException(
HttpStatus.BAD_REQUEST,
"error",
FixturesLoader.getFixtures().get("AuthenticatePasswordInvalidCredentialsErrorResponse"),
utf8
)
);
}
Stub
LoginActivity
@Rule
public SpicedTestRule<LoginActivity> mTestRule = new SpicedTestRule<>(LoginActivity.class);
private void registerInvalidCredentialsLoginRequest() {
Charset utf8 = Charset.forName("utf-8");
mTestRule.registerResponseFor(
LoginRequest.class,
new HttpClientErrorException(
HttpStatus.BAD_REQUEST,
"error",
FixturesLoader.getFixtures().get("AuthenticatePasswordInvalidCredentialsErrorResponse"),
utf8
)
);
}
Functional test
@Test
public void unauthorizedUserLogin() {
registerInvalidCredentialsLoginRequest();
setUserAndPassword();
clickOnSignIn();
Context context = InstrumentationRegistry.getTargetContext();
String errorMessage = context.getString(R.string.InvalidCredentialsError);
onView(allOf(withText(errorMessage), withParent(withId(R.id.errors_wrapper)))).
check(matches(isDisplayed()));
}
LoginActivity
Functional test
@Test
public void unauthorizedUserLogin() {
registerInvalidCredentialsLoginRequest();
setUserAndPassword();
clickOnSignIn();
Context context = InstrumentationRegistry.getTargetContext();
String errorMessage = context.getString(R.string.InvalidCredentialsError);
onView(allOf(withText(errorMessage), withParent(withId(R.id.errors_wrapper)))).
check(matches(isDisplayed()));
}
LoginActivity
Espresso Usage
private UserSession mUserSession = CurrentUserSessionTestHelper.buildUserSession();
private void setUserAndPassword() {
onView(ViewMatchers.withId(R.id.email)).perform(typeText(mUserSession.getEmail()));
onView(withId(R.id.password)).perform(typeText("password"));
}
private void clickOnSignIn() {
onView(withId(R.id.action_login)).perform(click());
}
LoginActivity
Instrumentation
@Test
public void unauthorizedUserLogin() {
registerInvalidCredentialsLoginRequest();
setUserAndPassword();
clickOnSignIn();
Context context = InstrumentationRegistry.getTargetContext();
String errorMessage = context.getString(R.string.InvalidCredentialsError);
onView(allOf(withText(errorMessage), withParent(withId(R.id.errors_wrapper)))).
check(matches(isDisplayed()));
}
LoginActivity
Instrumentation
@Test
public void unauthorizedUserLogin() {
registerInvalidCredentialsLoginRequest();
setUserAndPassword();
clickOnSignIn();
Context context = InstrumentationRegistry.getTargetContext();
String errorMessage = context.getString(R.string.InvalidCredentialsError);
onView(allOf(withText(errorMessage), withParent(withId(R.id.errors_wrapper)))).
check(matches(isDisplayed()));
}
LoginActivity
Espresso usage
@Test
public void unauthorizedUserLogin() {
registerInvalidCredentialsLoginRequest();
setUserAndPassword();
clickOnSignIn();
Context context = InstrumentationRegistry.getTargetContext();
String errorMessage = context.getString(R.string.InvalidCredentialsError);
onView(allOf(withText(errorMessage), withParent(withId(R.id.errors_wrapper)))).
check(matches(isDisplayed()));
}
LoginActivity
Espresso usage
onView(
allOf(
withText(errorMessage),
withParent(
withId(R.id.errors_wrapper)
)
)
).check(
matches(
isDisplayed()
)
);
LoginActivity
Espresso usage
onView(
allOf(
withText(errorMessage),
withParent(
withId(R.id.errors_wrapper)
)
)
).check(
matches(
isDisplayed()
)
);
LoginActivity
Espresso usage
onView(
allOf(
withText(errorMessage),
withParent(
withId(R.id.errors_wrapper)
)
)
).check(
matches(
isDisplayed()
)
);
LoginActivity
Espresso usage
onView(
allOf(
withText(errorMessage),
withParent(
withId(R.id.errors_wrapper)
)
)
).check(
matches(
isDisplayed()
)
);
LoginActivity
Espresso usage
onView(
allOf(
withText(errorMessage),
withParent(
withId(R.id.errors_wrapper)
)
)
).check(
matches(
isDisplayed()
)
);
LoginActivity
Espresso usage
onView(
allOf(
withText(errorMessage),
withParent(
withId(R.id.errors_wrapper)
)
)
).check(
matches(
isDisplayed()
)
);
LoginActivity
Espresso usage
onView(
allOf(
withText(errorMessage),
withParent(
withId(R.id.errors_wrapper)
)
)
).check(
matches(
isDisplayed()
)
);
LoginActivity
Espresso usage
onView(
allOf(
withText(errorMessage),
withParent(
withId(R.id.errors_wrapper)
)
)
).check(
matches(
isDisplayed()
)
);
LoginActivity
Intents
Functional test - Landing Activity
@Test
public void testOnEnterAction() {
Intent result = new Intent();
intending(hasComponent(hasClassName(equalTo(LoginActivity.class.getName())))).
respondWith(
new Instrumentation.ActivityResult(Activity.RESULT_OK, result)
);
intending(hasComponent(hasClassName(equalTo(HomeActivity.class.getName())))).
respondWith(
new Instrumentation.ActivityResult(Activity.RESULT_CANCELED, null)
);
clickOn(R.id.action_enter);
intended(hasComponent(hasClassName(equalTo(HomeActivity.class.getName()))));
}
Intents
Functional test - Landing Activity
@Test
public void testOnEnterAction() {
Intent result = new Intent();
intending(hasComponent(hasClassName(equalTo(LoginActivity.class.getName())))).
respondWith(
new Instrumentation.ActivityResult(Activity.RESULT_OK, result)
);
intending(hasComponent(hasClassName(equalTo(HomeActivity.class.getName())))).
respondWith(
new Instrumentation.ActivityResult(Activity.RESULT_CANCELED, null)
);
clickOn(R.id.action_enter);
intended(hasComponent(hasClassName(equalTo(HomeActivity.class.getName()))));
}
Intents
Functional test - Landing Activity
@Test
public void testOnEnterAction() {
Intent result = new Intent();
intending(hasComponent(hasClassName(equalTo(LoginActivity.class.getName())))).
respondWith(
new Instrumentation.ActivityResult(Activity.RESULT_OK, result)
);
intending(hasComponent(hasClassName(equalTo(HomeActivity.class.getName())))).
respondWith(
new Instrumentation.ActivityResult(Activity.RESULT_CANCELED, null)
);
clickOn(R.id.action_enter);
intended(hasComponent(hasClassName(equalTo(HomeActivity.class.getName()))));
}
Intents
Functional test - Landing Activity
@Test
public void testOnEnterAction() {
Intent result = new Intent();
intending(hasComponent(hasClassName(equalTo(LoginActivity.class.getName())))).
respondWith(
new Instrumentation.ActivityResult(Activity.RESULT_OK, result)
);
intending(hasComponent(hasClassName(equalTo(HomeActivity.class.getName())))).
respondWith(
new Instrumentation.ActivityResult(Activity.RESULT_CANCELED, null)
);
clickOn(R.id.action_enter);
intended(hasComponent(hasClassName(equalTo(HomeActivity.class.getName()))));
}
Intents
Functional test - Landing Activity
@Test
public void testOnEnterAction() {
Intent result = new Intent();
intending(hasComponent(hasClassName(equalTo(LoginActivity.class.getName())))).
respondWith(
new Instrumentation.ActivityResult(Activity.RESULT_OK, result)
);
intending(hasComponent(hasClassName(equalTo(HomeActivity.class.getName())))).
respondWith(
new Instrumentation.ActivityResult(Activity.RESULT_CANCELED, null)
);
clickOn(R.id.action_enter);
intended(hasComponent(hasClassName(equalTo(HomeActivity.class.getName()))));
}
Android
Components
Utilities
Android system
UI
Stubs
Android
Components
Utilities
Android system
UI
Stubs
Unit testing
● jUnit
● Instrumentation
Android
Components
Utilities
Android system
UI
Stubs
Using manual or library-assisted dependency injection to
switch dependencies for stubs when testing
Unit testing
● jUnit
● Instrumentation
Android
Components
Utilities
Android system
UI
Stubs
Espresso to manipulate and unit-
test views in an isolated Activity
Using manual or library-assisted dependency injection to
switch dependencies for stubs when testing
Unit testing
● jUnit
● Instrumentation
Android
Components
Utilities
Android system
UI
Stubs
Espresso to manipulate and unit-
test views in an isolated Activity
UI automator
Functional test of interactions
between the app and the Android
system
Using manual or library-assisted dependency injection to
switch dependencies for stubs when testing
Unit testing
● jUnit
● Instrumentation
Android
Components
Utilities
Android system
UI
Stubs
Espresso to manipulate and unit-
test views in an isolated Activity
Functional testing
● Espresso
● Espresso Intents
● Instrumentation
● Stubs
UI automator
Functional test of interactions
between the app and the Android
system
Using manual or library-assisted dependency injection to
switch dependencies for stubs when testing
Unit testing
● jUnit
● Instrumentation
...then 2 + 2 = 4 so...
We can test on Android!
References
https://developer.android.com/training/testing.html
http://developer.android.com/reference/android/app/Instrumentation.html
https://developer.android.com/tools/testing-support-library/index.html
Thanks to
Ariel, Gian, Juanma, Michel
@moove_it
Questions?

A guide to Android automated testing