Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Harshit Bangar

Harshit Bangar

I am going to talk about how to write scalable ui tests and what are the things you can do with it.

Avatar for bangarharshit

bangarharshit

October 12, 2016

More Decks by bangarharshit

Other Decks in Programming

Transcript

  1. What do we need • Espresso • Gradle build variants

    • OKHttp • Dagger 2 • UIAutomator • Screenshot test
  2. 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; } }; }
  3. 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; } }; }
  4. Espresso Recipes • 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); } } }
  5. Espresso Recipes • ScreenShot public static void takeScreenshot (String name,

    Activity activity) { View scrView = activity.getWindow().getDecorView().getRootView(); scrView.setDrawingCacheEnabled (true); Bitmap bitmap = Bitmap.createBitmap(scrView.getDrawingCache ()); scrView.setDrawingCacheEnabled (false); // Write to file. } @Before public void setUp() throws Exception { setFailureHandler( new CustomFailureHandler(getInstrumentation().getTargetContext())); }
  6. Espresso Recipes • ScreenShot public static void takeScreenshot(String name, Activity

    activity) { View scrView = activity.getWindow().getDecorView().getRootView(); scrView.setDrawingCacheEnabled(true); Bitmap bitmap = Bitmap.createBitmap(scrView.getDrawingCache()); scrView.setDrawingCacheEnabled(false); // Write to file. } @Before public void setUp() throws Exception { setFailureHandler (new CustomFailureHandler (getInstrumentation ().getTargetContext ())); }
  7. Dagger 2 if (BuildConfig.DEBUG) { return new DebugApi(); } else

    { return new Api(); } @Module public final class DataModule { @Provides @Singleton public Api provideApi() { return new Api(); } } @Module public final class DebugDataModule { @Provides @Singleton Api provideApi() { return new DebugApi(); //Very heavy contains tons of dependecy like STETHO. } }
  8. Dagger 2 if (BuildConfig.DEBUG) { return new DebugApi(); } else

    { return new Api(); } @Module public final class DataModule { @Provides @Singleton public Api provideApi() { return new Api(); } } @Module public final class DebugDataModule { @Provides @Singleton Api provideApi() { return new DebugApi(); //Very heavy contains tons of dependecy like STETHO. } }
  9. Dagger 2 if (BuildConfig.DEBUG) { return new DebugApi(); } else

    { return new Api(); } @Module public final class DataModule { @Provides @Singleton public Api provideApi() { return new Api(); } } @Module public final class DebugDataModule { @Provides @Singleton Api provideApi() { return new DebugApi(); //Very heavy contains tons of dependecy like STETHO. } }
  10. Dagger 2 @Singleton @Component(modules = {DataModule.class}) public interface Graph {

    void inject(BaseActivity activity); public final static class Initializer { public static Graph init(boolean mockMode) { return Dagger_Graph.builder() .build(); } } } @Singleton @Component(modules = {DebugDataModule.class}) public interface Graph { void inject(BaseActivity activity); void inject(InjectedActivityTest test); public final static class Initializer { public static Graph init(boolean mockMode) { return Dagger_Graph.builder() .debugDataModule(new DebugDataModule(mockMode)) .build(); } } }
  11. Dagger 2 @Singleton @Component(modules = {DataModule.class}) public interface Graph {

    void inject(BaseActivity activity); public final static class Initializer { public static Graph init(boolean mockMode) { return Dagger_Graph.builder() .build(); } } } @Singleton @Component(modules = {DebugDataModule.class}) public interface Graph { void inject(BaseActivity activity); void inject(InjectedActivityTest test); public final static class Initializer { public static Graph init(boolean mockMode) { return Dagger_Graph.builder() .debugDataModule(new DebugDataModule(mockMode)) .build(); } } }
  12. Espresso Recipe • A/B Tests. 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); }
  13. Espresso Recipe • A/B Tests. 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); }
  14. Espresso Recipe • A/B Tests. 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(); } } @Experiment(name="XYZ", treatment="ABC") @Test public void test() { // Test code. }
  15. Espresso Recipe • A/B Tests. 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(); } } @Experiment(name="XYZ", treatment="ABC") @Test public void test() { // Test code. }
  16. Espresso Recipe • IdlingResource CountingIdlingResource mCountingIdlingResource = new CountingIdlingResource ("Generic");

    public void beforeLongTask() { mCountingIdlingResource.increment(); } public void afterLongTask() { mCountingIdlingResource.decrement(); } // Setup Espresso.registerIdlingResources(mCountingIdlingResource);
  17. Espresso Recipe • IdlingResource CountingIdlingResource mCountingIdlingResource = new CountingIdlingResource("Generic"); public

    void beforeLongTask () { mCountingIdlingResource. increment(); } public void afterLongTask() { mCountingIdlingResource.decrement(); } // Setup Espresso.registerIdlingResources(mCountingIdlingResource);
  18. Espresso Recipe • IdlingResource CountingIdlingResource mCountingIdlingResource = new CountingIdlingResource("Generic"); public

    void beforeLongTask() { mCountingIdlingResource.increment(); } public void afterLongTask() { mCountingIdlingResource. decrement(); } // Setup Espresso.registerIdlingResources(mCountingIdlingResource);
  19. Espresso Recipe • IdlingResource CountingIdlingResource mCountingIdlingResource = new CountingIdlingResource("Generic"); public

    void beforeLongTask() { mCountingIdlingResource.increment(); } public void afterLongTask() { mCountingIdlingResource.decrement(); } // Setup Espresso.registerIdlingResources(mCountingIdlingResource);
  20. Idling resource • ThreadPoolExectuor class EspressoCountingIdlingResource extends ThreadPoolExecutor implements IdlingResource

    { @Override public synchronized void execute(Runnable r) { super.execute(r); threadCount++; } @Override protected synchronized void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); threadCount--; if (threadCount == 0) { if (null != callback) { callback.onTransitionToIdle(); } } } @Override public void registerIdleTransitionCallback(ResourceCallback callback) { this.callback = callback; } }
  21. Idling resource • ThreadPoolExectuor class EspressoCountingIdlingResource extends ThreadPoolExecutor implements IdlingResource

    { @Override public synchronized void execute(Runnable r) { super.execute(r); threadCount++; } @Override protected synchronized void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); threadCount--; if (threadCount == 0) { if (null != callback) { callback.onTransitionToIdle(); } } } @Override public void registerIdleTransitionCallback(ResourceCallback callback) { this.callback = callback; } }
  22. Idling resource • ThreadPoolExectuor class EspressoCountingIdlingResource extends ThreadPoolExecutor implements IdlingResource

    { @Override public synchronized void execute(Runnable r) { super.execute(r); threadCount++; } @Override protected synchronized void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); threadCount--; if (threadCount == 0) { if (null != callback) { callback.onTransitionToIdle(); } } } @Override public void registerIdleTransitionCallback(ResourceCallback callback) { this.callback = callback; } }
  23. Idling resource • ThreadPoolExectuor class EspressoCountingIdlingResource extends ThreadPoolExecutor implements IdlingResource

    { @Override public synchronized void execute(Runnable r) { super.execute(r); threadCount++; } @Override protected synchronized void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); threadCount--; if (threadCount == 0) { if (null != callback) { callback.onTransitionToIdle(); } } } @Override public void registerIdleTransitionCallback(ResourceCallback callback) { this.callback = callback; } }
  24. Espresso Recipe • 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.
  25. Espresso Recipe • 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.
  26. Espresso Recipe • Replay 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() { }
  27. Espresso Recipe • Replay 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() { }
  28. Espresso Recipe • Robot class PaymentRobot { PaymentRobot amount(long amount)

    { ... } PaymentRobot recipient(String recipient) { .. } ResultRobot send() { ... } } class ResultRobot { ResultRobot isSuccess() { ... } } PaymentRobot payment = new PaymentRobot(); ResultRobot result = payment .amount(42_00) .recipient("[email protected]") .send(); result.isSuccess();
  29. Espresso Recipe • Robot class PaymentRobot { PaymentRobot amount(long amount)

    { ... } PaymentRobot recipient(String recipient) { .. } ResultRobot send() { ... } } class ResultRobot { ResultRobot isSuccess() { ... } } PaymentRobot payment = new PaymentRobot(); ResultRobot result = payment .amount(42_00) .recipient("[email protected]") .send(); result.isSuccess();
  30. Espresso Recipes • Performance Testing @Override public View getView(int position,

    View convertView, ViewGroup parent ) { Contact contact = getItem(position); LayoutInflater inflater = LayoutInflater.from(getContext()); // This line is wrong, we're inflating a new view always instead of only if it's null. // For demonstration purposes, we will leave this here to show the resulting jank. convertView = inflater.inflate(R.layout.item_contact, parent, false); TextView contactName = (TextView) convertView.findViewById(R.id.contact_name); ImageView contactImage = (ImageView) convertView.findViewById(R.id.contact_image); contactName.setText(contact.getName()); // Let's just create another bitmap when we need one. This makes no attempts to re-use // bitmaps that were previously used in rendering past list view elements, causing a large // amount of memory to be consumed as you scroll farther down the list. Bitmap bm = BitmapFactory.decodeResource(convertView.getResources(), R.drawable.bbq); contactImage.setImageBitmap(bm); return convertView; }
  31. Espresso Recipes • Performance Testing @Override public View getView(int position,

    View convertView, ViewGroup parent ) { Contact contact = getItem(position); LayoutInflater inflater = LayoutInflater.from(getContext()); // This line is wrong, we're inflating a new view always instead of only if it's null. // For demonstration purposes, we will leave this here to show the resulting jank. convertView = inflater.inflate(R.layout.item_contact, parent, false); TextView contactName = (TextView) convertView.findViewById(R.id.contact_name); ImageView contactImage = (ImageView) convertView.findViewById(R.id.contact_image); contactName.setText(contact.getName()); // Let's just create another bitmap when we need one. This makes no attempts to re-use // bitmaps that were previously used in rendering past list view elements, causing a large // amount of memory to be consumed as you scroll farther down the list. Bitmap bm = BitmapFactory.decodeResource(convertView.getResources(), R.drawable.bbq); contactImage.setImageBitmap(bm); return convertView; }
  32. Espresso Recipes • Performance Testing @Override public View getView(int position,

    View convertView, ViewGroup parent ) { Contact contact = getItem(position); LayoutInflater inflater = LayoutInflater.from(getContext()); // This line is wrong, we're inflating a new view always instead of only if it's null. // For demonstration purposes, we will leave this here to show the resulting jank. convertView = inflater.inflate(R.layout.item_contact, parent, false); TextView contactName = (TextView) convertView.findViewById(R.id.contact_name); ImageView contactImage = (ImageView) convertView.findViewById(R.id.contact_image); contactName.setText(contact.getName()); // Let's just create another bitmap when we need one. This makes no attempts to re-use // bitmaps that were previously used in rendering past list view elements, causing a large // amount of memory to be consumed as you scroll farther down the list. Bitmap bm = BitmapFactory.decodeResource(convertView.getResources(), R.drawable.bbq); contactImage.setImageBitmap(bm); return convertView; }
  33. Espresso Recipes • Performance Testing @Test @PerfTest public void scrollFullList()

    throws InterruptedException { ListView listView = (ListView) mActivityRule.getActivity().findViewById(android.R.id.list); // Get last position and offset for zero-indexed position tracking. int lastPosition = listView.getAdapter().getCount() - 1; // Espresso method of scrolling to the last item. // onData(anything()).atPosition(lastPosition); // Standard Android method of scrolling to the last position. listView.smoothScrollToPositionFromTop(lastPosition, 0, SCROLL_TIME_IN_MILLIS); // Scrolling is performed asynchronously so we need to periodically loop and detect if // we're finished scrolling yet. This can be delayed by any work being done to display // data items in the ListView. while (listView.getLastVisiblePosition() != lastPosition) { Thread.sleep(300); } } @Rule public EnableTestTracing mEnableTestTracing = new EnableTestTracing(); @Rule public EnablePostTestDumpsys mEnablePostTestDumpsys = new EnablePostTestDumpsys(); @Rule public EnableLogcatDump mEnableLogcatDump = new EnableLogcatDump(); @Rule public EnableNetStatsDump mEnableNetStatsDump = new EnableNetStatsDump();
  34. Espresso Recipes • Performance Testing @Test @PerfTest public void scrollFullList()

    throws InterruptedException { ListView listView = (ListView) mActivityRule.getActivity().findViewById(android.R.id.list); // Get last position and offset for zero-indexed position tracking. int lastPosition = listView.getAdapter().getCount() - 1; // Espresso method of scrolling to the last item. // onData(anything()).atPosition(lastPosition); // Standard Android method of scrolling to the last position. listView.smoothScrollToPositionFromTop(lastPosition, 0, SCROLL_TIME_IN_MILLIS); // Scrolling is performed asynchronously so we need to periodically loop and detect if // we're finished scrolling yet. This can be delayed by any work being done to display // data items in the ListView. while (listView.getLastVisiblePosition() != lastPosition) { Thread.sleep(300); } } @Rule public EnableTestTracing mEnableTestTracing = new EnableTestTracing(); @Rule public EnablePostTestDumpsys mEnablePostTestDumpsys = new EnablePostTestDumpsys(); @Rule public EnableLogcatDump mEnableLogcatDump = new EnableLogcatDump(); @Rule public EnableNetStatsDump mEnableNetStatsDump = new EnableNetStatsDump();
  35. UIAutomator private static final String BASIC_SAMPLE_PACKAGE = "com.google.android.youtube" ; private

    static final int LAUNCH_TIMEOUT = 5000; private UiDevice mDevice; private static double totalTime = 0; public static final String YOUTUBE_BUTTON_ID = "com.google.android.youtube:id/menu_search" ; private long startTime = 0; @Before public void startMainActivityFromHomeScreen() { // Initialize UiDevice instance mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); // Start from the home screen mDevice.pressHome(); // Wait for launcher final String launcherPackage = mDevice.getLauncherPackageName(); assertThat(launcherPackage, notNullValue()); mDevice.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)), LAUNCH_TIMEOUT); // Launch the app Context context = InstrumentationRegistry.getContext(); final Intent intent = context.getPackageManager() .getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE); // Clear out any previous instances intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); context.startActivity(intent); // Wait for the app to appear startTime = System.nanoTime(); mDevice.wait(Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)), LAUNCH_TIMEOUT); }
  36. UIAutomator private static final String BASIC_SAMPLE_PACKAGE = "com.google.android.youtube"; private static

    final int LAUNCH_TIMEOUT = 5000; private UiDevice mDevice; private static double totalTime = 0; public static final String YOUTUBE_BUTTON_ID = "com.google.android.youtube:id/menu_search"; private long startTime = 0; @Before public void startMainActivityFromHomeScreen () { // Initialize UiDevice instance mDevice = UiDevice. getInstance (InstrumentationRegistry. getInstrumentation ()); // Start from the home screen mDevice. pressHome (); // Wait for launcher final String launcherPackage = mDevice. getLauncherPackageName (); assertThat (launcherPackage, notNullValue ()); mDevice. wait(Until.hasObject (By.pkg(launcherPackage). depth(0)), LAUNCH_TIMEOUT); // Launch the app Context context = InstrumentationRegistry. getContext (); final Intent intent = context. getPackageManager () . getLaunchIntentForPackage (BASIC_SAMPLE_PACKAGE); // Clear out any previous instances intent. addFlags (Intent.FLAG_ACTIVITY_CLEAR_TASK); context. startActivity (intent); // Wait for the app to appear startTime = System. nanoTime (); mDevice. wait(Until.hasObject (By.pkg(BASIC_SAMPLE_PACKAGE). depth(0)), LAUNCH_TIMEOUT); }
  37. UIAutomator @Test public void youtubeTest() throws UiObjectNotFoundException, InterruptedException { while

    (true) { UiObject okButton = mDevice.findObject(new UiSelector() .resourceId(YOUTUBE_BUTTON_ID)); // Simulate a user-click on the OK button, if found. if(okButton.exists() && okButton.isEnabled()) { okButton.click(); totalTime = totalTime + (System.nanoTime() - startTime)/Math.pow(10, 9); i(YoutubeTest.class.getSimpleName(), String.valueOf(totalTime)); break; } else { Thread.sleep(5); } } } @After public void cleanup() throws UiObjectNotFoundException { UiObject app = mDevice.findObject(new UiSelector().packageName(BASIC_SAMPLE_PACKAGE)); app.swipeLeft(100); }
  38. UIAutomator @Test public void youtubeTest() throws UiObjectNotFoundException, InterruptedException { while

    (true) { UiObject okButton = mDevice.findObject(new UiSelector() .resourceId(YOUTUBE_BUTTON_ID)); // Simulate a user-click on the OK button, if found. if(okButton.exists() && okButton.isEnabled()) { okButton.click(); totalTime = totalTime + (System.nanoTime() - startTime)/Math.pow(10, 9); i(YoutubeTest.class.getSimpleName(), String.valueOf(totalTime)); break; } else { Thread.sleep(5); } } } @After public void cleanup() throws UiObjectNotFoundException { UiObject app = mDevice.findObject(new UiSelector().packageName(BASIC_SAMPLE_PACKAGE)); app.swipeLeft(100); }
  39. ScreenShot test public void testRendering() throws Throwable { LayoutInflater inflater

    = LayoutInflater. from(getInstrumentation ().getTargetContext ()); View view = inflater. inflate(R.layout.search_bar, null, false); ViewHelpers. setupView(view) .setExactWidthDp (300) .layout(); Screenshot.snap(view) .record(); } gradle screenshotTests gradle recordMode screenshotTests gradle verifyMode screenshotTests
  40. ScreenShot test public void testRendering() throws Throwable { LayoutInflater inflater

    = LayoutInflater.from(getInstrumentation().getTargetContext()); View view = inflater.inflate(R.layout.search_bar, null, false); ViewHelpers.setupView(view) .setExactWidthDp(300) .layout(); Screenshot.snap(view) .record(); } gradle screenshotTests gradle recordMode screenshotTests gradle verifyMode screenshotTests