Android Espresso:测试首选项(多个列表视图)

Android Espresso:测试首选项(多个列表视图),android,android-espresso,android-testing,android-instrumentation,Android,Android Espresso,Android Testing,Android Instrumentation,我试图测试我的设置活动类,但我一直得到一个模糊的ViewMatcherException 下面是我的测试用例: @Test public void whenHospitalSettingEmpty_shouldDisplaySummary() throws Exception { Resources res = getInstrumentation().getTargetContext().getResources(); mPreferenceEditor.putString(

我试图测试我的
设置活动
类,但我一直得到一个
模糊的ViewMatcherException

下面是我的测试用例:

@Test
public void whenHospitalSettingEmpty_shouldDisplaySummary() throws Exception {
    Resources res = getInstrumentation().getTargetContext().getResources();

    mPreferenceEditor.putString(
            res.getString(R.string.activity_settings_hospital_key),
            "");

    mActivityTestRule.launchActivity(null);

    String expected = res.getString(R.string.activity_settings_hospital_summary);

    onData(allOf(
            is(instanceOf(Preference.class)),
            withKey(res.getString(R.string.activity_settings_hospital_key)),
            withSummary(R.string.activity_settings_hospital_summary),
            withTitle(R.string.activity_settings_hospital_title)))
            .onChildView(withText(expected))
            .check(matches(isDisplayed()));

}
以下是日志输出:

android.support.test.espresso.AmbiguousViewMatcherException: 'is assignable from class: class android.widget.AdapterView' matches multiple views in the hierarchy.
Problem views are marked with '****MATCHES****' below.

View Hierarchy:
...
+------->LinearLayout{id=16909142, res-name=headers, visibility=VISIBLE, width=600, height=887, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=2}
|
+-------->ListView{id=16908298, res-name=list, visibility=VISIBLE, width=536, height=823, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, root-is-layout-requested=false, has-input-connection=false, x=32.0, y=32.0, child-count=0} ****MATCHES******
|
+-------->FrameLayout{id=16909143, res-name=list_footer, visibility=VISIBLE, width=536, height=0, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, root-is-layout-requested=false, has-input-connection=false, x=32.0, y=855.0, child-count=0}
|
+------>RelativeLayout{id=16909146, res-name=button_bar, visibility=GONE, width=0, height=0, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=true, is-selected=false, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=2}
|
+------->AppCompatButton{id=16909147, res-name=back_button, visibility=VISIBLE, width=0, height=0, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=false, is-focusable=true, is-layout-requested=true, is-selected=false, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, text=Tilbage, input-type=0, ime-target=false, has-links=false}
|
+------->LinearLayout{id=-1, visibility=VISIBLE, width=0, height=0, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=true, is-selected=false, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=2}
|
+-------->AppCompatButton{id=16909148, res-name=skip_button, visibility=GONE, width=0, height=0, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=false, is-focusable=true, is-layout-requested=true, is-selected=false, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, text=Spring over, input-type=0, ime-target=false, has-links=false}
|
+-------->AppCompatButton{id=16909149, res-name=next_button, visibility=VISIBLE, width=0, height=0, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=false, is-focusable=true, is-layout-requested=true, is-selected=false, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, text=Næste, input-type=0, ime-target=false, has-links=false}
|
+----->LinearLayout{id=-1, visibility=VISIBLE, width=600, height=887, has-focus=true, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=3}
|
+------>ListView{id=16908298, res-name=list, visibility=VISIBLE, width=600, height=887, has-focus=true, has-focusable=true, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=true, is-focusable=true, is-layout-requested=false, is-selected=false, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=12} ****MATCHES******
|
+------->AppCompatTextView{id=16908310, res-name=title, visibility=VISIBLE, width=584, height=35, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, root-is-layout-requested=false, has-input-connection=false, x=8.0, y=0.0, text=Synkronisering, input-type=0, ime-target=false, has-links=false}
...
出于某种原因,
onData()
匹配两个
ListView
s,但我不知道如何防止这种情况,因为我不知道第一个
ListView
来自哪里

这是我的
首选项.xml

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <PreferenceCategory
        android:title="@string/activity_settings_sync_category_title"
        android:key="@string/activity_settings_sync_category_key">

        <SwitchPreference
            android:key="@string/activity_settings_sync_key"
            android:summary="@string/activity_settings_sync_summary"
            android:title="@string/activity_settings_sync_title"
            android:defaultValue="true"/>

    </PreferenceCategory>

    <PreferenceCategory
        android:title="@string/activity_settings_site_category_title"
        android:key="@string/activity_settings_site_category_key"
        android:summary="@string/activity_settings_site_category_summary">


        <EditTextPreference
            android:key="@string/activity_settings_hospital_key"
            android:title="@string/activity_settings_hospital_title"
            android:summary="@string/activity_settings_hospital_summary"
            android:dialogTitle="@string/activity_settings_hospital_dialog_title"
            android:dialogMessage="@string/activity_settings_hospital_dialog_message"
            android:inputType="textCapWords" />

        <EditTextPreference
            android:key="@string/activity_settings_department_key"
            android:title="@string/activity_settings_department_title"
            android:summary="@string/activity_settings_department_summary"
            android:dialogTitle="@string/activity_settings_department_dialog_title"
            android:dialogMessage="@string/activity_settings_department_dialog_message"
            android:inputType="textCapWords" />

    </PreferenceCategory>

    <PreferenceCategory
        android:title="@string/activity_settings_notification_ringtone_category_title"
        android:key="@string/activity_settings_notification_ringtone_category_key">


        <RingtonePreference
            android:key="@string/activity_settings_notification_ringtone_key"
            android:title="@string/activity_settings_notification_ringtone_title"
            android:summary="@string/activity_settings_notification_ringtone_summary"
            android:dialogTitle="@string/activity_settings_notification_ringtone_dialog_title"
            android:dialogMessage="@string/activity_settings_notification_ringtone_dialog_message"/>

    </PreferenceCategory>

    <PreferenceCategory
        android:title="@string/activity_settings_dicom_category_title"
        android:key="@string/activity_settings_dicom_category_key">

        <SwitchPreference
                android:key="@string/activity_settings_dicom_worklist_key"
                android:title="@string/activity_settings_dicom_worklist_title"
                android:summary="@string/activity_settings_dicom_worklist_summary" />

            <!--<EditTextPreference-->
                <!--android:key="dcmwl_ae_title_preference"-->
                <!--android:title="@string/pref_dicom_settings_worklist_ae_title"-->
                <!--android:dialogMessage="@string/pref_dicom_settings_worklist_ip_dialog_text"-->
                <!--android:dialogTitle="@string/pref_dicom_settings_worklist_ip_dialog_title"-->
                <!--android:dependency="dcmwl_enable"/>-->
            <!--<EditTextPreference-->
                <!--android:defaultValue="10.0.0.2"-->
                <!--android:key="dcmwl_ip_preference"-->
                <!--android:title="@string/pref_dicom_settings_worklist_ip"-->
                <!--android:dependency="dcmwl_enable" />-->
            <!--<EditTextPreference-->
                <!--android:key="dcmwl_port_preference"-->
                <!--android:title="@string/pref_dicom_settings_worklist_port"-->
                <!--android:dependency="dcmwl_enable"/>-->

        <SwitchPreference
            android:key="@string/activity_settings_dicom_pacs_key"
            android:title="@string/activity_settings_dicom_pacs_title"
            android:summary="@string/activity_settings_dicom_pacs_summary"/>

            <!--<EditTextPreference-->
                <!--android:key="pacs_ae_title_preference"-->
                <!--android:title="@string/pref_dicom_settings_pacs_ae_title"-->
                <!--android:dependency="pacs_enable"/>-->
            <!--<EditTextPreference-->
                <!--android:defaultValue="10.0.0.2"-->
                <!--android:key="pacs_ip_preference"-->
                <!--android:title="@string/pref_dicom_settings_pacs_ip"-->
                <!--android:dependency="pacs_enable"/>-->
            <!--<EditTextPreference-->
                <!--android:key="pacs_port_preference"-->
                <!--android:title="@string/pref_dicom_settings_pacs_port"-->
                <!--android:dependency="pacs_enable"/>-->

    </PreferenceCategory>

    <PreferenceCategory
        android:title="@string/activity_settings_debug_category_title"
        android:key="@string/activity_settings_debug_category_key">

        <SwitchPreference
            android:key="@string/activity_settings_debug_key"
            android:summary="@string/activity_settings_debug_summary"
            android:title="@string/activity_settings_debug_title"
            android:defaultValue="false"/>

    </PreferenceCategory>

</PreferenceScreen>
但它仍然不起作用。日志输出如下所示:

private static Matcher<View> withResName(final String resName) {

        return new TypeSafeMatcher<View>() {
            @Override
            public void describeTo(Description description) {
                description.appendText("with res-name: " + resName);
                Timber.d("Resourcename: ", resName);
            }

            @Override
            public boolean matchesSafely(View view) {
                Timber.d("View ID: ", view.getId());
                String matchableResName = view.getResources().getResourceEntryName(view.getId());
                return !TextUtils.isEmpty(matchableResName) && matchableResName.equals(resName);
            }
        };
    }
...
I/TestRunner: android.content.res.Resources$NotFoundException:
 Unable to find resource ID #0xffffffff
    at android.content.res.Resources.getResourceEntryName(Resources.java:2127)
    at com.conhea.smartgfr.preferences.SettingsActivityTest$2.matchesSafely(SettingsActivityTest.java:153)
    at com.conhea.smartgfr.preferences.SettingsActivityTest$2.matchesSafely(SettingsActivityTest.java:143)
    at org.hamcrest.TypeSafeMatcher.matches(TypeSafeMatcher.java:65)
    at org.hamcrest.core.IsNot.matches(IsNot.java:25)
    at android.support.test.espresso.matcher.ViewMatchers$26.matchesSafely(ViewMatchers.java:892)
    at android.support.test.espresso.matcher.ViewMatchers$26.matchesSafely(ViewMatchers.java:883)
    at org.hamcrest.TypeSafeMatcher.matches(TypeSafeMatcher.java:65)
...

我想
PreferenceCategory
将在内部创建
ListView
。因此,您的层次结构中会有多个
AdapterView
s,因此会发生
模糊的ViewMatcherException

只要您不拥有那些
ListView
s的
id
,您就不能执行与
ViewMatcher.withId()的匹配。因此,您必须基于这些
ListView
s之间的重要内容创建自定义匹配器

不幸的是,那些
ListView
s具有相同的属性,因此您必须在父级执行匹配。这些
ListView
s的父级是
LinearLayout
。您可能会注意到,其中一个(实际上是您不感兴趣的一个)具有属性
res name=headers

这将提示您在具有父项的
列表视图上执行匹配,该父项的资源名称为而不是“headers”

让我们试着用浓缩咖啡来做。首先,让我们编写一个匹配器,将
视图
与特定的资源名称匹配:



    class ResourceNameMatcher extends TypeSafeMatcher {

      private final String resName;

      public ResourceNameMatcher(String resName) {
        this.resName = resName;
      }

      @Override protected boolean matchesSafely(View item) {
        Resources resources = item.getResources();
        String matchableResName = resources.getResourceName(item.getId());
        return !TextUtils.isEmpty(matchableResName) && matchableResName.equals(resName);
      }

      @Override public void describeTo(Description description) {
        description.appendText("with res-name: " + resName);
      }
    }



      onData(allOf(
                   Matchers.is(instanceOf(Preference.class),
                   ... // other matchers here )))
          .onChildView(withText(expected))
          .inAdapterView(withParent(not(new ResourceNameMatcher("headers"))))
          .check(matches(isDisplayed()));

现在,让我们在具有特定资源名称的
视图上执行匹配:



    class ResourceNameMatcher extends TypeSafeMatcher {

      private final String resName;

      public ResourceNameMatcher(String resName) {
        this.resName = resName;
      }

      @Override protected boolean matchesSafely(View item) {
        Resources resources = item.getResources();
        String matchableResName = resources.getResourceName(item.getId());
        return !TextUtils.isEmpty(matchableResName) && matchableResName.equals(resName);
      }

      @Override public void describeTo(Description description) {
        description.appendText("with res-name: " + resName);
      }
    }



      onData(allOf(
                   Matchers.is(instanceOf(Preference.class),
                   ... // other matchers here )))
          .onChildView(withText(expected))
          .inAdapterView(withParent(not(new ResourceNameMatcher("headers"))))
          .check(matches(isDisplayed()));


请注意,这只是一个草图代码,可能不起作用,只要我没有测试它。不过,这个概念应该是可行的。

这是我测试设置活动的解决方案

测试:

@Test
public void whenHospitalSettingEmpty_shouldDisplaySummary() throws Exception {
    Resources res = getInstrumentation().getTargetContext().getResources();

    // Set the hospital preferencevalue to empty
    mPreferenceEditor.putString(
            res.getString(R.string.activity_settings_hospital_key),
            "").commit();

    // Launch activity
    mActivityTestRule.launchActivity(null);

    // When hospital value is empty we want to display the summary from our string ressources
    // instead of our preferencevalue
    onPreferenceRow(allOf(
                withKey(res.getString(R.string.activity_settings_hospital_key)),
                withSummary(R.string.activity_settings_hospital_summary)))
            .check(matches(isDisplayed()));
}
我的助手方法:

...

private static Matcher<View> withResName(final String resName) {

    return new TypeSafeMatcher<View>() {
        @Override
        public void describeTo(Description description) {
            description.appendText("with res-name: " + resName);
        }

        @Override
        public boolean matchesSafely(View view) {
            int identifier = view.getResources().getIdentifier(resName, "id", "android");
            return !TextUtils.isEmpty(resName) && (view.getId() == identifier);
        }
    };
}

private static DataInteraction onPreferenceRow(Matcher<? extends Object> datamatcher) {

    DataInteraction interaction = onData(datamatcher);

    return interaction
        .inAdapterView(allOf(
            withId(android.R.id.list),
            not(withParent(withResName("headers")))));
}
。。。
resName专用静态匹配器(最终字符串resName){
返回新的TypeSafeMatcher(){
@凌驾
公共无效说明(说明){
description.appendText(“带res name:+resName”);
}
@凌驾
公共布尔匹配安全(视图){
int identifier=view.getResources().getIdentifier(resName,“id”,“android”);
return!TextUtils.isEmpty(resName)&&(view.getId()==标识符);
}
};
}
private静态数据交互onPreferenceRow(MatcherThis不起作用。item.getId()没有返回ID。由于某种原因,android.R.ID.headers ID被隐藏。这真的很奇怪,因为在布局检查器中,我可以看到linearlayout有一个名为headers的ID,并且在调试输出中我可以看到整数值。而不是
item.getId()
perform
resources.getIdentifier(“headers”、“ID”、“android”)
getIdentifier()是解决方案的一部分。感谢您的帮助。