浓咖啡:多个ListViews
我试图测试我的SettingsActivity
类,但我不断收到AmbiguousViewMatcherException
。浓咖啡:多个ListViews
这里是我的测试用例:
@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()
2个ListView
s比,但我只是无法弄清楚如何防止这种情况,因为我不明白第一个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)
...
我想PreferenceCategory
将在内部创建ListView
。因此,您最终会在您的层次中有多个AdapterView
,因此发生AmbiguousViewMatcherException
。
只要你做不具备与ListView
S的id
,你不能执行与ViewMatcher.withId()
匹配。因此,您必须根据这些ListView
之间的重要事项创建自定义匹配器。
不幸的是,那些ListView
s具有相同的属性,因此您必须在父级别上执行匹配。那些ListView
的父母是LinearLayout
。正如你可能会注意到的,其中一个(实际上是你不感兴趣的)有一个属性res-name=headers
。
这会给你一个提示执行上ListView
匹配,有一个家长,其中资源名称是不“头”。
让我们试着用浓咖啡来做到这一点。一等品,让我们写一个匹配,一个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()));
注,这只是一个草图代码,只要我没有测试过,它可能不起作用。不过,这个概念应该起作用。
这是我的解决方案,用于测试我的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")))));
}
这是行不通的。 item.getId()没有返回一个ID。出于某种原因,android.R.id.headers id是隐藏的。这非常奇怪,因为在布局检查器中,我可以看到,linearlayout有一个名为headers的ID,在调试输出中我可以看到整数值。 – Bohsen
getIdentifier()是解决方案的一部分,而不是'item.getId()'执行'resources.getIdentifier(“headers”,“id”,“android”)' – azizbekian
。谢谢您的帮助。 – Bohsen