Android Espresso:如何封装自定义组件视图查找逻辑以复用代码?
Absolutely! Encapsulating this lookup logic is a smart move—it cuts down on repetitive code and makes your tests way easier to maintain, especially if your custom component's layout changes down the line. Here's how to do it properly:
1. Create a Reusable Helper Function
First, centralize the lookup logic in a helper class or object. This way, every test that needs to access the EditText in your custom component just calls this function instead of repeating the entire allOf chain.
If you want to keep using internal IDs but centralize them (so you only update once if IDs change), here's an example:
object CustomComponentEspressoHelpers { // If you ever change R.id.editText to something else, just update it here private val INTERNAL_EDIT_TEXT_ID = R.id.editText fun getEditTextInCustomComponent(componentId: Int): ViewInteraction { return onView( allOf( withId(INTERNAL_EDIT_TEXT_ID), isDescendantOfA(withId(componentId)) ) ) } }
Then in your tests, replace the long lookup line with:
val editText = CustomComponentEspressoHelpers.getEditTextInCustomComponent(R.id.mobileEdt)
2. Make It Resilient to Layout Changes
If you want to avoid relying on internal IDs entirely (so even if you rename R.id.editText or restructure the component's layout, your tests don't break), use attribute-based matchers instead. For example, if your custom component has exactly one AppCompatEditText, target it by type:
object CustomComponentEspressoHelpers { fun getEditTextInCustomComponent(componentId: Int): ViewInteraction { return onView( allOf( isAssignableFrom(AppCompatEditText::class.java), isDescendantOfA(withId(componentId)) ) ) } }
If your component has multiple EditTexts, add more specific matchers to narrow it down—like using withHint(R.string.your_hint) or withInputType(InputType.TYPE_CLASS_TEXT):
fun getEditTextInCustomComponent(componentId: Int): ViewInteraction { return onView( allOf( isAssignableFrom(AppCompatEditText::class.java), withHint(R.string.mobile_number_hint), isDescendantOfA(withId(componentId)) ) ) }
3. Bonus: If Your Custom Component Is a Subclass
If you've wrapped your layout in a custom View subclass (e.g., class MobileEditTextComponent : ConstraintLayout(context)), you can make the matcher even cleaner by targeting the component class directly instead of its ID:
fun getEditTextInCustomComponent(componentMatcher: Matcher<View>): ViewInteraction { return onView( allOf( isAssignableFrom(AppCompatEditText::class.java), isDescendantOfA(componentMatcher) ) ) }
Use it in tests like this:
// Target by ID val editText = getEditTextInCustomComponent(withId(R.id.mobileEdt)) // Or target by tag if you have multiple instances val editText = getEditTextInCustomComponent(withTagValue(`is`(R.id.mobile_edit_tag)))
Why This Works
- Less duplication: No more copying the same
allOfchain across multiple tests. - Easier maintenance: If your component's layout changes (e.g., you rename internal IDs or add new views), you only update the helper function once, not every test.
- Cleaner tests: Your test code focuses on what you're testing, not the nitty-gritty of view lookup.
内容的提问来源于stack exchange,提问作者Rasoul Miri




