android testing support library: the nitty gritty - zan markan - codemotion milan 2016
TRANSCRIPT
© Zan Markan 2016 - @zmarkan
Android Testing Support Library
The Nitty Gritty
© Zan Markan 2016 - @zmarkan
Testing 101• on JVM vs on device• unit / integration / functional / end to end• Robolectric, Calabash, Instrumentation, Robotium,
Appium
© Zan Markan 2016 - @zmarkan
"Support"• Android framework vs Support libraries• Trend to unbundle• support-v4, appcompat-v7, recyclerview, ...
© Zan Markan 2016 - @zmarkan
"The support library is basically a mountain of hacks"— Chris Banes, Google
© Zan Markan 2016 - @zmarkan
Android Testing Support Library
© Zan Markan 2016 - @zmarkan
Good Old Times...• jUnit3 syntax• Remember
ActivityInstrumentationTestCase2<MainActivit>?
© Zan Markan 2016 - @zmarkan
More jUnit3 goodness• overriding setUp() and tearDown()
• testPrefixedMethods() & test_prefixedMethods()
• Ignorance Inheritance is bliss
© Zan Markan 2016 - @zmarkan
Welcome to the present• jUnit4 syntax• No more extending
• @Test, @Before, @After, @AfterClass,...
• ActivityTestRule, InstrumentationRegistry
© Zan Markan 2016 - @zmarkan
What else is in there?• Espresso• More Espresso (there's a lot to it)• UIAutomator• Test Runner• Test Rules• ...
© Zan Markan 2016 - @zmarkan
What else is in there?
© Zan Markan 2016 - @zmarkan
But First...The setup(note: AS does that on it's own if you create a new project - these instructions will mostly apply for legacy projects)
© Zan Markan 2016 - @zmarkan
Gradle• Set your test runner to be AndroidJUnitRunner• Add dependencies• Voila!
© Zan Markan 2016 - @zmarkan
Error:Conflict with dependency 'com.android.support:support-annotations'.
Resolved versions for app (23.2.1) and test app (23.1.1) differ.
See http://g.co/androidstudio/app-test-app-conflict for details.
© Zan Markan 2016 - @zmarkan
Gradle• Set your test runner to be AndroidJUnitRunner• Add dependencies• Voila!• Resolve dependencies
© Zan Markan 2016 - @zmarkan
Dependency resolutions• App and Test app depend on different lib versions
• Run ./gradlew :app:dependencies
• ! in the compile and androidTestCompile tasks
© Zan Markan 2016 - @zmarkan
Resolve with• Force dependency versions in the test APK• exclude dependency (everywhere applicable)• use Resolution strategy
© Zan Markan 2016 - @zmarkan
Force versions in test APK// Resolve conflicts between main and test APK:androidTestCompile "com.android.support:support-annotations:$rootProject.supportLibraryVersion"androidTestCompile "com.android.support:support-v4:$rootProject.supportLibraryVersion"
Source: github.com/googlesamples/android-architecture/blob/todo-mvp/todoapp/app/build.gradle
© Zan Markan 2016 - @zmarkan
Gradle• Set your test runner to be AndroidJUnitRunner• Add dependencies• Resolve dependencies• Voila!
© Zan Markan 2016 - @zmarkan
© Zan Markan 2016 - @zmarkan
Espresso - components• View interactions & assertions• Hamcrest syntax• No (unnecessary) waits
© Zan Markan 2016 - @zmarkan
Espresso - API
Cheat sheet:google.github.io/android-testing-support-library/docs/espresso/cheatsheet
© Zan Markan 2016 - @zmarkan
Poking the screenonView(withId(R.id.button)).perform(click());
© Zan Markan 2016 - @zmarkan
Poking the screenonView(withId(R.id.button)).perform(click());
• allOf, anyOf, ...
• withParent, withText...
• isDisplayed, isDialog...
© Zan Markan 2016 - @zmarkan
Espresso - contrib• RecyclerView• Drawers• Pickers• Accessibility
© Zan Markan 2016 - @zmarkan
Espresso++• Custom matchers• Custom ViewActions• Idling resources• Page objects• Intent mocking
© Zan Markan 2016 - @zmarkan
Custom matchers• Find a view in the hierarchy• Good for custom views & components• Override:
• describeTo
• matchesSafely
© Zan Markan 2016 - @zmarkan
Custom matchersinterface MyView{ boolean hasCustomProperty();}
static BoundedMatcher withCustomPropery() { return new BoundedMatcher<Object, MyView>(MyView.class){
@Override public void describeTo(Description description) { description.appendText("Custom property is enabled"); }
@Override protected boolean matchesSafely(MyView item) { return item.hasCustomProperty(); } };}
© Zan Markan 2016 - @zmarkan
Custom ViewActions• Same story as matchers, just a bit more extensive
• example allows us to scroll in NestedScrollView• github.com/zmarkan/Android-Espresso-
ScrollableScroll
© Zan Markan 2016 - @zmarkan
Custom ViewActions - APIpublic class ScrollableUtils {
public static ViewAction scrollableScrollTo() { return actionWithAssertions(new ScrollableScrollToAction()); }}
© Zan Markan 2016 - @zmarkan
Implementation...public class ScrollableScrollToAction implements ViewAction{ private static final String TAG = com.zmarkan.nestedscroll.action.ScrollableScrollToAction.class.getSimpleName();
@SuppressWarnings("unchecked") @Override public Matcher<View> getConstraints() { return allOf(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE), isDescendantOfA(anyOf( isAssignableFromClassOrInterface(ScrollingView.class)))); }
@Override public void perform(UiController uiController, View view) { if (isDisplayingAtLeast(90).matches(view)) { Log.i(TAG, "View is already displayed. Returning."); return; } Rect rect = new Rect(); view.getDrawingRect(rect); if (!view.requestRectangleOnScreen(rect, true /* immediate */)) { Log.w(TAG, "Scrolling to view was requested, but none of the parents scrolled."); } uiController.loopMainThreadUntilIdle(); if (!isDisplayingAtLeast(90).matches(view)) { throw new PerformException.Builder() .withActionDescription(this.getDescription()) .withViewDescription(HumanReadables.describe(view)) .withCause(new RuntimeException( "Scrolling to view was attempted, but the view is not displayed")) .build(); } }
@Override public String getDescription() { return "scroll to"; }}
© Zan Markan 2016 - @zmarkan
Implementation...@Overridepublic Matcher<View> getConstraints() { return allOf(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE), isDescendantOfA(anyOf( isAssignableFromClassOrInterface(ScrollingView.class))));}
© Zan Markan 2016 - @zmarkan
Implementation...@Overridepublic void perform(UiController uiController, View view) { if (isDisplayingAtLeast(90).matches(view)) { Log.i(TAG, "View is already displayed. Returning."); return; } Rect rect = new Rect(); view.getDrawingRect(rect); if (!view.requestRectangleOnScreen(rect, true /* immediate */)) { Log.w(TAG, "Scrolling to view was requested, but none of the parents scrolled."); } uiController.loopMainThreadUntilIdle(); if (!isDisplayingAtLeast(90).matches(view)) { throw new PerformException.Builder() .withActionDescription(this.getDescription()) .withViewDescription(HumanReadables.describe(view)) .withCause(new RuntimeException( "Scrolling to view was attempted, but the view is not displayed")) .build(); }}
© Zan Markan 2016 - @zmarkan
Don't panic.• Take something that works
• ...like scrollTo() in regular Espresso for ScrollView• modify it• profit
© Zan Markan 2016 - @zmarkan
Custom IdlingResource• A better way to wait• Use when you have background stuff going on• Override:
• getName
• isIdleNow
• registerIdleTransitionCallback
© Zan Markan 2016 - @zmarkan
Custom IdlingResourcepublic class CustomIdlingResource implements IdlingResource{
private ResourceCallback resourceCallback; private EventBus bus; private boolean loadingCompleted = false;
public CustomIdlingResource(EventBus bus){ bus.register(this); }
public void onEvent(LoadingCompletedEvent event){ loadingCompleted = true; isIdleNow(); }}
© Zan Markan 2016 - @zmarkan
Custom IdlingResource @Override public String getName() { return CustomIdlingResource.class.getName(); }
@Override public void registerIdleTransitionCallback( ResourceCallback resourceCallback) { this.resourceCallback = resourceCallback; }
@Override public boolean isIdleNow() { boolean idle = loadingCompleted; if (idle && resourceCallback != null) { resourceCallback.onTransitionToIdle(); bus.unregister(this); } return idle; }}
© Zan Markan 2016 - @zmarkan
Page Object Pattern• Build your own DSL• Every screen is a Page• Page-specific actions and verifications• (as seen in Calabash, etc...)
© Zan Markan 2016 - @zmarkan
Page Classpublic class MainViewTestUtils {
public void enterUserName(String text){ /* espresso goes here */ } public void enterPassword(String text){ /* ... */ } public void pressContinue(){ /* ... */ } public void assertErrorShown(boolean shown){ /* ... */ }}
© Zan Markan 2016 - @zmarkan
Page Class in test@Testpublic void errorShownWhenPasswordIncorrect(){
MainViewTestUtils view = new MainViewTestUtils();
view.enterUsername(username); view.enterPassword(incorrectPassword); view.pressContinue(); view.assertErrorShown(true);}
© Zan Markan 2016 - @zmarkan
To boldly go...
© Zan Markan 2016 - @zmarkan
UI Automator• Interact with any installed app• Use to create full end-to-end tests• Can coexist with Espresso in the same app
• Use uiautomatorviewer command to find items in the hierarchy
© Zan Markan 2016 - @zmarkan
UI Automator viewer
© Zan Markan 2016 - @zmarkan
UI Automator syntaxUiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation();
device.pressHome();
device.findObject(new UiSelector().descriptionContains("Google search")).clickAndWaitForNewWindow();
© Zan Markan 2016 - @zmarkan
Test Runner
© Zan Markan 2016 - @zmarkan
Test Runner• Spins up tests...• and runs them• Customise to prepare mocks• Easier run/debug via command line
© Zan Markan 2016 - @zmarkan
Extending the Runner• Use custom Application class• Provide mocked dependencies• Specify this new runner in build.gradle• Kotlin Test Runner
© Zan Markan 2016 - @zmarkan
Extending the Runnerpublic class CustomRunner extends AndroidJUnitRunner{ @Override public Application newApplication(ClassLoader cl, String className, Context context) throws InstantiationException, IllegalAccessException, ClassNotFoundException { return Instrumentation.newApplication(TestApplication.class, context); }}
© Zan Markan 2016 - @zmarkan
Running on the Command line• Run module/package/class/method• Run Large/Medium/Small test only• Shard to run in parallel• Debug without reinstalling
© Zan Markan 2016 - @zmarkan
Simple API• send commands to runner via ADB (adb shell
[commands])
• am instrument -w -e class your.full.classname#methodName your.test.package.name/your.test.Runner.class
• d.android.com/tools/testing/testing_otheride.html
© Zan Markan 2016 - @zmarkan
Test Rules
© Zan Markan 2016 - @zmarkan
Test Rules• Set starting activity / Service
• Replace ActivityInstrumentationTestCase2• (in most cases)• Add / Extend to create more components
© Zan Markan 2016 - @zmarkan
Test Rules - examples• MockWebServerRule - sets up MockWebServer
when required• Source: github.com/artem-zinnatullin/
qualitymatters
© Zan Markan 2016 - @zmarkan
© Zan Markan 2016 - @zmarkan
Firebase test lab• Simple setup• CI support (via gcloud)• Support for VMs• firebase.google.com/docs/test-lab• Robo test for automated testing
© Zan Markan 2016 - @zmarkan
Espresso Test Recorder• Since AS 2.2 preview 3• Generates test code after clicking on screen• (Not necessarily nice code)• tools.android.com/tech-docs/test-recorder
© Zan Markan 2016 - @zmarkan
Above & Beyond?• Espresso Web for WebViews• JankTestHelper• Stuff is being added.
© Zan Markan 2016 - @zmarkan
Best of all?
© Zan Markan 2016 - @zmarkan
It's all open source!
© Zan Markan 2016 - @zmarkan
Pusher/ˈpʊʃ ər/
noun1. Platform of APIs for building highly connected apps2. Hiring in London !
© Zan Markan 2016 - @zmarkan
fin• ! www.spacecowboyrocketcompany.com
• " @zmarkan
• # zan at markan dot me
• $ androidchat.co (zmarkan)
• %& @zmarkan
© Zan Markan 2016 - @zmarkan
© Zan Markan 2016 - @zmarkan
• google.github.io/android-testing-support-library/contribute
• Quality Matters - Artem Zinnatulin • d.android.com/tools/testing-support-library• github.com/googlesamples/android-testing• chiuki.github.io/advanced-android-espresso
© Zan Markan 2016 - @zmarkan
• Espresso: Brian Legate (Flickr)• Hyperdrive: Youtube• Road Runner: BrownZelip (Flickr)• Back to the future: Youtube• Titanic / unit tests passing: Twitter
© Zan Markan 2016 - @zmarkan