Advanced Android Espresso 陳 チェン 釗 チュー 琪 キ [Chiu-Ki Chan] @chiuki

+ChiuKiChan http://bit.ly/advesp

What is Espresso?

Automatic UI testing

Espresso Android Testing Support Library

Simulate user interactions

Automatic synchronization of test actions with app UI

No need to sleep

Hello World

Formula onView( ViewMatcher ) .perform( ViewAction ) .check( ViewAssertion );

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

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

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

More info github.com/chiuki/espresso-samples under hello-world

Espresso library onView( withId (R.id.greet_button) ) .perform( click() ) .check( matches (not( isEnabled() ) );

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

Combining matchers

Toolbar title

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 blog.sqisland.com/2015/05/espresso-match-toolbar-title.html

github.com/chiuki/espresso-samples under toolbar-title

onData

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

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);

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

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); } });

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 onData 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>

with actionOnItem with Matcher<View>

More info github.com/chiuki/espresso-samples under list-view-basic

github.com/chiuki/espresso-samples under recycler-view-basic

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); }

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); }

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")));

Summary Matcher , ViewAction , ViewAssertion

, , Combining matchers

Custom matchers

ListView

RecyclerView

Idling Resource

Dagger and Mockito