Android Espresso试用小结
昨天把GATC2014的视频看了,发现Google终于会继续更新Espresso了,正好总结一下以前试用的心得。
照着官方指南(https://code.google.com/p/android-test-kit/wiki/Espresso)搭好环境,可能要FQ,不过百度也能搜到一些配置的文章。遇到两个问题:一是报错Dex Loader Unable to execute dex: Multiple dex files define什么的,这个是因为其他project里面包含了guava的jar包,我把重复的删掉了,也可以不用集成jar包,每个都去下最新的;二是报错Dex Loader Unable to execute dex: Cannot merge new index 65780 into a non-jumbo instruction!,在project.properties里面添加dex.force.jumbo=true就行了。
测试的app里面是一层层的folder,里面一个个的document,这些都是用ListView显示的,Espresso提供了通过position点击的API。
1 public void clickOnList(int position) { 2 onData(Matchers.anything()) 3 .inAdapterView(withId(android.R.id.list)) 4 .atPosition(position) 5 .perform(ViewActions.click()); 6 }
但是这样很不方便,因为我可能希望通过匹配名称或路径来点击,或者我希望点击后得到名称或者路径。名称数据是存储在JSONObject里面的,我可以先找到AdpterView,通过遍历adapter来获取text对应的position,然后再用Espresso提供的API来点击。
1 public static int position; 2 3 public void FolderDocument (String text) { 4 onView(withId(android.R.id.list)).check(ViewAssertions.matches(FolderDocumentData(Matchers.is(text)))); 5 clickOnList(position); 6 } 7 8 public static Matcher<View> FolderDocumentData(final Matcher<String> dataMatcher) { 9 10 return new TypeSafeMatcher<View>() { 11 12 public void describeTo(Description description) { 13 description.appendText("project document data: "); 14 dataMatcher.describeTo(description); 15 } 16 17 @Override 18 public boolean matchesSafely(View view) { 19 if (!(view instanceof AdapterView)) { 20 return false; 21 } 22 adapter = ((AdapterView) view).getAdapter(); 23 for (int i = 0; i < adapter.getCount(); i++) { 24 android.util.Log.i("Hailin", adapter.getItem(i).toString()); 25 JSONObject a = (JSONObject) adapter.getItem(i); 26 try { 27 if (dataMatcher.matches(a.getString("n"))) { 28 position = i; 29 return true; 30 } 31 } catch (JSONException e) { 32 // TODO Auto-generated catch block 33 e.printStackTrace(); 34 } 35 } 36 return false; 37 } 38 }; 39 }
为了避免固定时间的空等以及长时间的render较大document,需要等待标志document render完成的View出现,并且Main Thread的状态为idle,然后再进行后续操作。下面为接口ViewAction的实现。
1 public static ViewAction waitView(final Matcher<View> matcher, final long millis) { 2 return new ViewAction() { 3 @Override 4 public Matcher<View> getConstraints() { 5 return isRoot(); 6 } 7 8 @Override 9 public String getDescription() { 10 return "wait for a specific view during " + millis + " millis."; 11 } 12 13 @Override 14 public void perform(final UiController uiController, final View view) { 15 uiController.loopMainThreadUntilIdle(); 16 final long startTime = System.currentTimeMillis(); 17 final long endTime = startTime + millis; 18 19 do { 20 for (View child : TreeIterables.breadthFirstViewTraversal(view)) { 21 // found view with required ID 22 if (matcher.matches(child)) { 23 return; 24 } 25 } 26 // uiController.loopMainThreadUntilIdle(); 27 uiController.loopMainThreadForAtLeast(50); 28 } 29 while (System.currentTimeMillis() < endTime); 30 31 // timeout happens 32 Log.i("Exception", "document/report execution times out"); 33 } 34 }; 35 }
有时候打开document的时候会弹出提示的dialog,为了保证automation能持续跑下去,可以匹配后点掉。
1 public static ViewAction checkUnexpectedView(final Matcher<View> matcher) { 2 return new ViewAction() { 3 @Override 4 public Matcher<View> getConstraints() { 5 return isRoot(); 6 } 7 8 @Override 9 public String getDescription() { 10 return "check whether there is an unexpected view."; 11 } 12 13 @Override 14 public void perform(final UiController uiController, final View view) { 15 uiController.loopMainThreadUntilIdle(); 16 17 for (View child : TreeIterables.breadthFirstViewTraversal(view)) { 18 // found view with required ID 19 if (matcher.matches(child)) { 20 UnexpectedViewFlag = true; 21 return; 22 } 23 } 24 // uiController.loopMainThreadUntilIdle(); 25 uiController.loopMainThreadForAtLeast(50); 26 } 27 }; 28 }
针对Phone和Tablet的不同,我需要给app设置orientation。
1 if (isTablet(mActivity)) { 2 onView(isRoot()).perform(setOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)); 3 } else { 4 onView(isRoot()).perform(setOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)); 5 } 6 7 public static boolean isTablet(Context context) { 8 return (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE; 9 } 10 11 public static ViewAction setOrientation(final int orientation) { 12 return new ViewAction() { 13 @Override 14 public Matcher<View> getConstraints() { 15 return isRoot(); 16 } 17 18 @Override 19 public String getDescription() { 20 return "check orientation"; 21 } 22 23 @Override 24 public void perform(final UiController uiController, final View view) { 25 uiController.loopMainThreadUntilIdle(); 26 27 final Activity activity = (Activity) view.getContext(); 28 activity.setRequestedOrientation(orientation); 29 uiController.loopMainThreadForAtLeast(50); 30 } 31 }; 32 }
测试时还有些其他问题:为了覆盖所有document,必须要遍历;测试跑出crash,需要设置skip list;crash后重启略过之前的跑过的document;跑测试的时候通过socket实时发log。这些跟Espresso无关,在这就不写了。