Espresso - Scalable UI Testing

Harshit Bangar

Objectives

  • Remove (or reduce) the need of sanity.

  • Protecting against backend failure.

  • More confidence in the development process.

  • Reliable testing using both backend and replay response.

Terminology

  • Espresso – Entry point to interactions with views (via onView and onData). Also exposes APIs that are not necessarily tied to any view (e.g. pressBack).

  • ViewMatchers – A collection of objects that implement Matcher<? super View> interface. You can pass one or more of these to the onView method to locate a view within the current view hierarchy.

  • ViewActions – A collection of ViewActions that can be passed to the ViewInteraction.perform() method (for example, click()).

  • ViewAssertions – A collection of ViewAssertions that can be passed the ViewInteraction.check() method. Most of the time, you will use the matches assertion, which uses a View matcher to assert the state of the currently selected view.

Example

onView(withId(R.id.my_view))      // withId(R.id.my_view) is a ViewMatcher
  .perform(click())               // click() is a ViewAction
  .check(matches(isDisplayed())); // matches(isDisplayed()) is a ViewAssertion

Finding a view:

onView(withId(R.id.my_view))
onView(allOf(withId(R.id.my_view), withText("Hello!")))
onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))))

Performing an action:

onView(...).perform(click());
onView(...).perform(typeText("Hello"), click());
onView(...).perform(scrollTo(), click());

Assertion:

Espresso - Recipes 

  • Custom View Matcher

  • Screenshot

Custom View Matchers

    public static Matcher<View> withCompoundDrawable(final int resourceId) {
        return new BoundedMatcher<View, TextView>(TextView.class) {
            @Override
            public void describeTo(Description description) {
                description.appendText("has compound drawable resource " + resourceId);
            }

            @Override
            public boolean matchesSafely(TextView textView) {
                for (Drawable drawable : textView.getCompoundDrawables()) {
                    if (sameBitmap(textView.getContext(), drawable, resourceId)) {
                        return true;
                    }
                }
                return false;
            }
        };
    }

Screenshot - When test fails.

Screenshot

//CustomFailureHandler to listen for test failure.

private static class CustomFailureHandler implements FailureHandler {
  private final FailureHandler delegate;

  public CustomFailureHandler(Context targetContext) {
    delegate = new ScreenShotFailureHandler(targetContext);
  }

  @Override
  public void handle(Throwable error, Matcher<View> viewMatcher) {
    try {
      delegate.handle(error, viewMatcher);
    } catch (NoMatchingViewException e) {
      throw new MySpecialException(e);
    }
  }
}

ScreenShot Delegate

public static void takeScreenshot(String name, Activity activity)
{

    // In Testdroid Cloud, taken screenshots are always stored
    // under /test-screenshots/ folder and this ensures those screenshots
    // be shown under Test Results
    String path = 
        Environment.getExternalStorageDirectory().getAbsolutePath() + "/test-screenshots/" + name + ”.png”;

    View scrView = activity.getWindow().getDecorView().getRootView();
    scrView.setDrawingCacheEnabled(true);
    Bitmap bitmap = Bitmap.createBitmap(scrView.getDrawingCache());
    scrView.setDrawingCacheEnabled(false);

    OutputStream out = null;
    File imageFile = new File(path);

    try {
        out = new FileOutputStream(imageFile);
        bitmap.compress(Bitmap.CompressFormat.PNG, 90, out);
        out.flush();
    } catch (FileNotFoundException e) {
        // exception
    } catch (IOException e) {
        // exception
    } finally {

        try {
            if (out != null) {
                out.close();
            }

        } catch (Exception exc) {
        }

    }
}

Rules

  • Experiment Rule

  • Authentical Rule

  • Replay Rule

AB Test

How do we run tests for different treatments?

AB Test

public interface ABTestManager {
   public void getExperimentTreatment(String experimentName);
   public void setExperimentTreatment(String experimentName, String treatment);
}

// Real Implementation populated by network response.

// Provided by Mocked dagger injector.
public class MockABTestManager implements ABTestManager {
   public void getExperimentTreatment(String experimentName);
   public void setExperimentTreatment(String experimentName, String treatment);
}

Experiment Rule

@Experiment(name="XYZ", treatment="ABC")
@Test
public void test() {
  // Test code.
}


public class ExperimentRule extends TestWatcher {

   @Inject MockABTestManager mockABTestManager;
   
   @Override
    protected void starting(Description description) {
          mockABTestManager.clear();
          Experiment experiment = description.getAnnotation(Experiment.class);
          mockABTestManager.put(experiment.getName(), experiment.getTreatment();
    }

}
  • Activities which requires session information before rendering.
  • State information is cleared after the tests ran and so a custom authentication rule.

Network replay and record

OkHttp Record Interceptor

class RecordInterceptor implements Interceptor {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Request request = chain.request();


    Response response = chain.proceed(request);
    
    String filename = request.getUri();
    // Serialised and store in file.

    return response;
  }
}

<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
</manifest>

// adb pull and store it in asset storage adapter.

Replay Interceptor

class ReplayInterceptor implements Interceptor {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Request request = chain.request();

    InputStream is = mContext.getResources().getAssets().open("your-file-path");
    BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
    String line;
    while ((line = br.readLine()) != null) {
                    //play with each line here 
        // Deserialiser and network response.
    }
    if (!TextUtils.isEmpty(response)) {
      return response;
    }
    Response response = chain.proceed(request);
    return response;
  }
}
@Replay(record=true)
@Test
public void test() { }

Reliable Test

  • CountingIdlingResource for network requests.

  • Replay and Record network response

  • Use replay and record - IDE.

Questions?
Hiring
Thank you!!

deck

By Harshit Bangar