click tracking

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
https://code.google.com/p/android-test-kit/wiki/EspressoV2CheatSheet

Hamcrest

Hamcrest general purpose matchers Hamcrest: Combining multiple matches, string matchers
http://www.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!