a guide to android automated testing
TRANSCRIPT
Utilities
Utilities are every piece of code, both pure java and Android-dependant, that are used as tools in the development process.
How to test
1. Using support libraries2. Stub code we can’t control while testing3. Decoupled code4. Testing everything that could be error-prone
How to test
1. Using support libraries2. Stub code we can’t control while testing3. Decoupled code4. Testing everything that could be error-prone
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.
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.
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.
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 libraries2. Stub code we can’t control while testing3. Decoupled code4. Testing everything that could be error-prone
Stub code we can’t control with testing
1. Robospice2. Card reader3. Thermal printer SDK4. Bluetooth
How to test
1. Using support libraries2. Stub code we can’t control while testing3. Decoupled code4. Testing everything that could be error-prone
Decoupled code
1. Android components only in charge of lifecycle
2. Layer for business logic3. Layer modeling views4. Create utilities for code that can be unit
tested
How to test
1. Using support libraries2. Stub code we can’t control while testing3. Decoupled code4. Testing everything that could be error-prone
Testing everything that could be error-prone
1. Do not test getter/setters2. Test code created by us3. Do not test third-party code (stub it or let it
be)
jUnitUnit test
private EmailValidator subject = new EmailValidator();
@Testpublic void doesNotValidateWithInvalidEmail() { String email = "invalidEmail";
assertThat(subject.isValid(email), is(false));}
jUnit + InstrumentationUnit test
// This component uses the Android framework out of our control so we need to use instrumentation to provide a contextprivate CurrentUserSessionService subject = new CurrentUserSessionService(InstrumentationRegistry.getTargetContext());
@Testpublic 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
Using manual or library-assisted dependency injection to switch dependencies for stubs when testing
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
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
@Testpublic 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
StubLoginActivity@Rulepublic 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 ) );}
StubLoginActivity@Rulepublic 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
@Testpublic 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
@Testpublic 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
@Testpublic 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
@Testpublic 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
@Testpublic 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
IntentsFunctional test - Landing Activity@Testpublic 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()))));}
IntentsFunctional test - Landing Activity@Testpublic 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()))));}
IntentsFunctional test - Landing Activity@Testpublic 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()))));}
IntentsFunctional test - Landing Activity@Testpublic 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()))));}
IntentsFunctional test - Landing Activity@Testpublic 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
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
Referenceshttps://developer.android.com/training/testing.htmlhttp://developer.android.com/reference/android/app/Instrumentation.htmlhttps://developer.android.com/tools/testing-support-library/index.html