我正在尝试测试我的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)
...
答案 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")))));
}