Android Native - How to match sibling in Espresso tests

dimitrilc 1 Tallied Votes 322 Views Share

Introduction

Finding a View in Espresso tests can be quite confusing because there are so many matchers available. In this tutorial, we will learn how to find a View based on its sibling contents.

Goals

At the end of the tutorial, you would have learned:

  1. How to match against siblings in Espresso tests.

Tools Required

  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 2.

Prerequisite Knowledge

  1. Basic Android.
  2. Basic Espresso.

Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. Replace the default activity_main.xml with the code below. This replaces the default TextView with a RecyclerView, constrains it, and assign it an id.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView_myRecycler"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
     </androidx.constraintlayout.widget.ConstraintLayout>
  3. Create a new sample layout called sample_viewholder.xml to act as the View for each RecyclerView item. This layout includes 3 TextView objects placed in a horizontal chain.

     <?xml version="1.0" encoding="utf-8"?>
     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/layout_viewHolder"
        android:layout_marginVertical="16dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    
        <TextView
            android:id="@+id/textView_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/textView_title"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="Name" />
    
        <TextView
            android:id="@+id/textView_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/textView_age"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toEndOf="@+id/textView_name"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="Title" />
    
        <TextView
            android:id="@+id/textView_age"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@+id/textView_title"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="Age" />
     </androidx.constraintlayout.widget.ConstraintLayout>
  4. Create a new data class called SampleViewHolderUiState to hold the UI state for the sample_viewholder.xml.

     data class SampleViewHolderUiState(
        val name: String,
        val title: String,
        val age: Int
     )
  5. Create a new MyAdapter to adapt to SampleViewHolderUiState.

     class MyAdapter(private val dataSet: List<SampleViewHolderUiState>)
        : RecyclerView.Adapter<MyAdapter.SampleViewHolder>() {
    
        class SampleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
            val textViewName: TextView = itemView.findViewById(R.id.textView_name)
            val textViewTitle: TextView = itemView.findViewById(R.id.textView_title)
            val textViewAge: TextView = itemView.findViewById(R.id.textView_age)
        }
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SampleViewHolder {
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.sample_viewholder, parent, false)
    
            return SampleViewHolder(view)
        }
    
        override fun onBindViewHolder(holder: SampleViewHolder, position: Int) {
            holder.textViewName.text = dataSet[position].name
            holder.textViewTitle.text = dataSet[position].title
            holder.textViewAge.text = "${dataSet[position].age}"
        }
    
        override fun getItemCount() = dataSet.size
     }
  6. Append the code below to initialize the RecyclerView in MainActivity#onCreate().

     val recyclerView = findViewById<RecyclerView>(R.id.recyclerView_myRecycler)
    
     val sampleDataSet = listOf(
        SampleViewHolderUiState("John", "Student", 17),
        SampleViewHolderUiState("Mary", "Teacher", 28)
     )
    
     recyclerView.adapter = MyAdapter(sampleDataSet)
  7. It is a good idea to run the App now to see if everything was set up correctly. It should look the same as the screenshot below.
    1.png

  8. To keep things simple, we will just reuse the pre-created ExampleInstrumentedTest file. Update it with the content below.

     @RunWith(AndroidJUnit4::class)
     class ExampleInstrumentedTest {
    
        @get:Rule
        val activityScenarioRule = ActivityScenarioRule(MainActivity::class.java)
    
        @Test
        fun test_matchSibling() {
            onView(withId(R.id.layout_viewHolder))
                .check(matches(isDisplayed()))
        }
     }

The Problem

If you attempt to run the test now, then it will fail with the error message below.

androidx.test.espresso.AmbiguousViewMatcherException: 'view.getId() is <2131230949/com.example.daniwebandroidnativematchsiblinginespresso:id/layout_viewHolder>' matches multiple views in the hierarchy.

This is because there are multiple views with the id of layout_viewHolder. Our RecyclerView has two different ViewHolders after all. Fortunately, they both contain different text values for all three TextView (name, title, age). To be able to find a specific View, then we will have to dig deeper into the view hierarchy and filter against those inner values.

Match against sibling children

It is possible to find the first layout_viewHolder View by matching against a sibling and its children. To match against a sibling, we need to use the hasSibling matcher. Replace the content of test_matchSibling with the code below.

@Test
fun test_matchSibling() {
   onView(allOf(
       withId(R.id.layout_viewHolder),
       hasSibling(allOf(
           withId(R.id.layout_viewHolder),
           withChild(allOf(
               withId(R.id.textView_name),
               withText("Mary"),
               hasSibling(allOf(
                   withId(R.id.textView_title),
                   withText("Teacher"),
                   hasSibling(allOf(
                       withId(R.id.textView_age),
                       withText("28")
                   ))
               ))
           ))
       ))
   ))
}

The code above is totally overkill because we actually do not have to go too deep into the sibling’s hierarchy nor match against the sibling’s id at all. I just wanted to demonstrate how nested hasSibling looks like when we need to use it. The code below can also find the same View in only one line.

onView(hasSibling(withChild(withText("Mary"))))

Match against own child siblings

We can also match the View against its own children hierarchy. The code below demonstrates this.

@Test
fun test_matchChildrenSiblings(){
   onView(allOf(
       withId(R.id.layout_viewHolder),
       withChild(allOf(
           withId(R.id.textView_name),
           withText("John"),
           hasSibling(allOf(
               withId(R.id.textView_title),
               withText("Student"),
               hasSibling(allOf(
                   withId(R.id.textView_age),
                   withText("17")
               ))
           ))
       ))
   )).check(matches(isDisplayed()))

   //One liner
   //onView(withChild(withText("John")))
}

Solution Code

ExampleInstrumentedTest.kt

@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {

   @get:Rule
   val activityScenarioRule = ActivityScenarioRule(MainActivity::class.java)

   @Test
   fun test_matchSibling() {
       onView(allOf(
           withId(R.id.layout_viewHolder),
           hasSibling(allOf(
               withId(R.id.layout_viewHolder),
               withChild(allOf(
                   withId(R.id.textView_name),
                   withText("Mary"),
                   hasSibling(allOf(
                       withId(R.id.textView_title),
                       withText("Teacher"),
                       hasSibling(allOf(
                           withId(R.id.textView_age),
                           withText("28")
                       ))
                   ))
               ))
           ))
       ))

       //One liner
       //onView(hasSibling(withChild(withText("Mary"))))
   }

   @Test
   fun test_matchChildrenSiblings(){
       onView(allOf(
           withId(R.id.layout_viewHolder),
           withChild(allOf(
               withId(R.id.textView_name),
               withText("John"),
               hasSibling(allOf(
                   withId(R.id.textView_title),
                   withText("Student"),
                   hasSibling(allOf(
                       withId(R.id.textView_age),
                       withText("17")
                   ))
               ))
           ))
       )).check(matches(isDisplayed()))

       //One liner
       //onView(withChild(withText("John")))
   }
}

MainActivity.kt

class MainActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       val recyclerView = findViewById<RecyclerView>(R.id.recyclerView_myRecycler)

       val sampleDataSet = listOf(
           SampleViewHolderUiState("John", "Student", 17),
           SampleViewHolderUiState("Mary", "Teacher", 28)
       )

       recyclerView.adapter = MyAdapter(sampleDataSet)
   }
}

MyAdapter.kt

class MyAdapter(private val dataSet: List<SampleViewHolderUiState>)
   : RecyclerView.Adapter<MyAdapter.SampleViewHolder>() {

   class SampleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
       val textViewName: TextView = itemView.findViewById(R.id.textView_name)
       val textViewTitle: TextView = itemView.findViewById(R.id.textView_title)
       val textViewAge: TextView = itemView.findViewById(R.id.textView_age)
   }

   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SampleViewHolder {
       val view = LayoutInflater.from(parent.context)
           .inflate(R.layout.sample_viewholder, parent, false)

       return SampleViewHolder(view)
   }

   override fun onBindViewHolder(holder: SampleViewHolder, position: Int) {
       holder.textViewName.text = dataSet[position].name
       holder.textViewTitle.text = dataSet[position].title
       holder.textViewAge.text = "${dataSet[position].age}"
   }

   override fun getItemCount() = dataSet.size
}

SampleViewHolderUiState

data class SampleViewHolderUiState(
   val name: String,
   val title: String,
   val age: Int
)

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <androidx.recyclerview.widget.RecyclerView
       android:id="@+id/recyclerView_myRecycler"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

sample_viewholder.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:id="@+id/layout_viewHolder"
   android:layout_marginVertical="16dp"
   android:layout_width="match_parent"
   android:layout_height="wrap_content">

   <TextView
       android:id="@+id/textView_name"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toStartOf="@+id/textView_title"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent"
       tools:text="Name" />

   <TextView
       android:id="@+id/textView_title"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toStartOf="@+id/textView_age"
       app:layout_constraintHorizontal_bias="0.5"
       app:layout_constraintStart_toEndOf="@+id/textView_name"
       app:layout_constraintTop_toTopOf="parent"
       tools:text="Title" />

   <TextView
       android:id="@+id/textView_age"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toEndOf="@+id/textView_title"
       app:layout_constraintTop_toTopOf="parent"
       tools:text="Age" />
</androidx.constraintlayout.widget.ConstraintLayout>

Summary

Congratulations, you have learned how to match against siblings in Espresso tests. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidNativeMatchSiblingInEspresso

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.