Advanced Android Espresso

チェン チュー [Chiu-Ki Chan]

@chiuki
+ChiuKiChan

http://bit.ly/advesp

What is Espresso?

Automatic UI testing

Look ma, no hands!

Espresso

  • Android Testing Support Library
  • Simulate user interactions
  • Automatic synchronization of test actions with app UI

No need to sleep
No need to sleep

Hello World

Formula

 onView(ViewMatcher) .perform(ViewAction) .check(ViewAssertion); 
Hello World

ViewMatcher

 onView(withId(R.id.greet_button)) .perform(ViewAction) .check(ViewAssertion); 
Hello World

ViewAction

 onView(withId(R.id.greet_button)) .perform(click()) .check(ViewAssertion); 
Hello World

ViewAssertion

 onView(withId(R.id.greet_button)) .perform(click()) .check(matches(not(isEnabled()));
Hello World

More info

Espresso library

 onView(withId(R.id.greet_button)) .perform(click()) .check(matches(not(isEnabled()));
Hello World

Hamcrest

 onView(withId(R.id.greet_button)) .perform(click()) .check(matches(not(isEnabled()));
Hello World

Espresso

Espresso ViewMatchers Espresso ViewActions and ViewAssertions
developer.android.com/training/testing/espresso/cheat-sheet.html

Hamcrest

Hamcrest general purpose matchers Hamcrest: Combining multiple matches, string matchers
marcphilipp.de/downloads/posts/2013-01-02-hamcrest-quick-reference/Hamcrest-1.3.pdf

Combining matchers

Toolbar title

Toolbar title

Hierarchy Viewer

Hierarchy Viewer

isAssignableFrom

 @Test public void toolbarTitle() { CharSequence title = InstrumentationRegistry .getTargetContext().getString(R.string.my_title); matchToolbarTitle(title); } private static ViewInteraction matchToolbarTitle( CharSequence title) { return onView( allOf( isAssignableFrom(TextView.class), withParent(isAssignableFrom(Toolbar.class)))) .check(matches(withText(title.toString()))); } 

Custom matchers

toolbar.getTitle()

 private static ViewInteraction matchToolbarTitle( CharSequence title) { return onView(isAssignableFrom(Toolbar.class)) .check(matches(withToolbarTitle(is(title)))); } 

toolbar.getTitle()

 private static Matcher<Object> withToolbarTitle( final Matcher<CharSequence> textMatcher) { return new BoundedMatcher<Object, Toolbar>(Toolbar.class) { @Override public boolean matchesSafely(Toolbar toolbar) { return textMatcher.matches(toolbar.getTitle()); } @Override public void describeTo(Description description) { description.appendText("with toolbar title: "); textMatcher.describeTo(description); } }; } 
  • Verify the Toolbar
  • TextMatcher instead of String

More info

onData

Formula

 onView(ViewMatcher) .perform(ViewAction) .check(ViewAssertion);
 onData(ObjectMatcher) .DataOptions .perform(ViewAction) .check(ViewAssertion);
Data Options

ListView

ListView

Item 27?

App

 final Item[] items = new Item[COUNT]; for (int i = 0; i < COUNT; ++i) { items[i] = new Item(i); } ArrayAdapter<Item> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, items); listView.setAdapter(adapter);
ListView

App

 public static class Item { private final int value; public Item(int value) { this.value = value; } public String toString() { return String.valueOf(value); } } 
ListView

App

 listView.setOnItemClickListener( new AdapterView .OnItemClickListener() { public void onItemClick( AdapterView<?> parent, View view, int position, long id) { textView.setText( items[position].toString()); textView.setVisibility(View.VISIBLE); } });
ListView

Test

 @Test public void clickItem() { onView(withId(R.id.text)) .check(matches(not(isDisplayed()))); onData(withValue(27)) .inAdapterView(withId(R.id.list)) .perform(click()); onView(withId(R.id.text)) .check(matches(withText("27"))) .check(matches(isDisplayed())); }

withValue

 public static Matcher<Object> withValue(final int value) { return new BoundedMatcher<Object, MainActivity.Item>(MainActivity.Item.class) { @Override public void describeTo(Description description) { description.appendText("has value " + value); } @Override public boolean matchesSafely( MainActivity.Item item) { return item.toString().equals(String.valueOf(value)); } }; } 

RecyclerView

RecyclerView

RecyclerView

onData
RecyclerView is not an AdapterView
RecyclerView is a ViewGroup,
not AdapterView

RecyclerViewActions

 @Test public void clickItem() { onView(withId(R.id.text)) .check(matches(not(isDisplayed()))); onView(withId(R.id.recycler_view)) .perform( RecyclerViewActions.actionOnItemAtPosition(27, click())); onView(withId(R.id.text)) .check(matches(withText("27"))) .check(matches(isDisplayed())); } 
actionOnItemAtPosition
 // ListView onData(withValue(27)) .inAdapterView(withId(R.id.list)) .perform(click());
  • actionOnHolderItem with Matcher<VH>
  • actionOnItem with Matcher<View>

More info

Idling Resource

Espresso Idle

  • No UI events queued
  • No tasks in AsyncTask thread pool

Custom IdlingResource

Define your own condition

e.g. IntentService is not running.

IntentServiceIdlingResource

 @Override public String getName() { return IntentServiceIdlingResource.class.getName(); }
 @Override public void registerIdleTransitionCallback( ResourceCallback resourceCallback) { this.resourceCallback = resourceCallback; }
 @Override public boolean isIdleNow() { boolean idle = !isIntentServiceRunning(); if (idle && resourceCallback != null) { resourceCallback.onTransitionToIdle(); } return idle; }

isIntentServiceRunning()

 private boolean isIntentServiceRunning() { ActivityManager manager = (ActivityManager) context.getSystemService( Context.ACTIVITY_SERVICE); for (ActivityManager.RunningServiceInfo info : manager.getRunningServices(Integer.MAX_VALUE)) { if (RepeatService.class.getName().equals( info.service.getClassName())) { return true; } } return false; }

Register

 @Before public void registerIntentServiceIdlingResource() { idlingResource = new IntentServiceIdlingResource( InstrumentationRegistry.getTargetContext()); Espresso.registerIdlingResources(idlingResource); } @After public void unregisterIntentServiceIdlingResource() { Espresso.unregisterIdlingResources(idlingResource); }

More info

Dagger and Mockito

Dagger

Dependency injection.

Different objects for app and test.

Mockito

Mock objects in test.

Dagger components

 public interface DemoComponent { void inject(MainActivity mainActivity); }
 @Singleton @Component(modules = ClockModule.class) public interface ApplicationComponent extends DemoComponent { }
 @Singleton @Component(modules = MockClockModule.class) public interface TestComponent extends DemoComponent { void inject(MainActivityTest mainActivityTest); }
Dagger

Application

 public class DemoApplication extends Application { private final DemoComponent component = createComponent(); protected DemoComponent createComponent() { return DaggerDemoApplication_ApplicationComponent.builder() .clockModule(new ClockModule()) .build(); } public DemoComponent component() { return component; } }

MockApplication

 public class MockDemoApplication extends DemoApplication { @Override protected DemoComponent createComponent() { return DaggerMainActivityTest_TestComponent.builder() .mockClockModule(new MockClockModule()) .build(); } }

MockTestRunner

 public class MockTestRunner extends AndroidJUnitRunner { @Override public Application newApplication( ClassLoader cl, String className, Context context) throws InstantiationException, IllegalAccessException, ClassNotFoundException { return super.newApplication( cl, MockDemoApplication.class.getName(), context); } }

build.gradle

 testInstrumentationRunner 'com.sqisland.android.test_demo.MockTestRunner'

Mockito

 /* App */ public DateTime getNow() { return new DateTime(); }
 /* Test */ Mockito.when(clock.getNow()) .thenReturn(new DateTime(2008, 9, 23, 0, 0, 0)); 
 /* Espresso */ onView(withId(R.id.date)) .check(matches(withText("2008-09-23"))); 

More info

Summary

  • Matcher, ViewAction, ViewAssertion
  • Combining matchers
  • Custom matchers
  • ListView
  • RecyclerView
  • Idling Resource
  • Dagger and Mockito

Friend Spell

github.com/chiuki/friendspell

  • Google Plus
  • Nearby API
  • Database


  • Dagger
  • Mockito
  • JUnit
  • UI-less instrumentation
  • Espresso

Google Plus comment
Friend Spell

Thank you!