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
deck
- 1,386