Android Espresso:测试首选项(多个ListViews)

时间:2017-05-29 10:30:22

标签: android android-espresso android-testing android-instrumentation

我正在尝试测试我的SettingsActivity课程,但我不断获得AmbiguousViewMatcherException

这是我的测试用例:

@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,但我无法弄清楚如何防止这种情况,因为我不明白第一个ListView来自哪里

这是我的preference.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);
            }
        };
    }

现在测试看起来像这样:

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

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

        mActivityTestRule.launchActivity(null);

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

//        onView(withText(expected)).check(matches(isDisplayed()));

        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))
                .inAdapterView(withParent(not(withResName("headers"))))
                .check(matches(isDisplayed()));


    }

但它仍然无效。日志输出如下所示:

...
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)
...

2 个答案:

答案 0 :(得分:1)

我认为PreferenceCategory会在内部创建ListView。因此,您的层次结构中最终会有多个AdapterView,因此AmbiguousViewMatcherException会发生。

只要您 拥有id的{​​{1}},就无法与ListView进行匹配。因此,您必须根据ViewMatcher.withId()之间的重要内容创建自定义匹配器。

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

这将为您提供在res-name=headers上进行匹配的提示,该ListView具有父级,资源名称 “headers”

让我们尝试用Espresso语言来做。第一,我们写一个匹配器,匹配一个具有特定资源名称的View



    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);
      }
    }


现在,让我们对View进行匹配,其中特定资源名称:



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


请注意,这只是一个草图代码,只要我没有测试过它就可能无效。然而,这个概念应该有效。

答案 1 :(得分:1)

这是我测试SettingsActivity的解决方案。

测试:

@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")))));
}