☕ Advanced Espresso Concepts 05 : Custom Matchers | by EspressoLab.Ai | Jun, 2024

In this blog, we’ll explore how to create custom matchers in Android UI testing using Espresso. Custom matchers allow you to define more precise and flexible conditions for finding and interacting with UI elements during testing. This guide will help you enhance your UI testing capabilities by showing you how to create and use custom matchers effectively.

Matchers in Espresso are used to find views in the UI hierarchy. While Espresso provides a set of built-in matchers, sometimes you need more flexibility to match views based on specific criteria that aren’t covered by the default matchers. This is where custom matchers come in handy.

To create a custom matcher, you’ll extend the functionality of the existing matchers in Espresso. This involves implementing the Matcher<View> interface.

1. Define Your Custom Matcher

Here’s an example of creating a custom matcher that matches a TextView with a specific drawable:

import android.view.View
import android.widget.TextView
import androidx.test.espresso.matcher.BoundedMatcher
import org.hamcrest.Description
import org.hamcrest.Matcher

fun withDrawable(resourceId: Int): Matcher<View> {
return object : BoundedMatcher<View, TextView>(TextView::class.java) {
override fun describeTo(description: Description) {
description.appendText("has drawable resource $resourceId")
}

override fun matchesSafely(textView: TextView): Boolean {
val drawables = textView.compoundDrawables
if (drawables[0] != null || drawables[1] != null || drawables[2] != null || drawables[3] != null) {
for (drawable in drawables) {
if (drawable != null) {
val expectedDrawable = textView.context.resources.getDrawable(resourceId, null)
if (drawable.constantState == expectedDrawable.constantState) {
return true
}
}
}
}
return false
}
}
}

2. Use Your Custom Matcher in Tests

You can now use this custom matcher in your tests to assert that a TextView has a specific drawable.

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId

class CustomMatcherTest {
@Test
fun testTextViewWithDrawable() {
onView(withId(R.id.my_text_view))
.check(matches(withDrawable(R.drawable.my_drawable)))
}
}

Example 1: Custom Matcher for View Background Color

This custom matcher checks if a view has a specific background color.

import android.graphics.drawable.ColorDrawable
import android.view.View
import androidx.test.espresso.matcher.BoundedMatcher
import org.hamcrest.Description
import org.hamcrest.Matcher

fun withBackgroundColor(color: Int): Matcher<View> {
return object : BoundedMatcher<View, View>(View::class.java) {
override fun describeTo(description: Description) {
description.appendText("has background color: $color")
}

override fun matchesSafely(view: View): Boolean {
val background = view.background
return background is ColorDrawable && background.color == color
}
}
}

Usage in Test:

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId

class CustomBackgroundColorMatcherTest {
@Test
fun testViewWithBackgroundColor() {
onView(withId(R.id.my_view))
.check(matches(withBackgroundColor(Color.RED)))
}
}

Example 2: Custom Matcher for TextView Font Size

This custom matcher checks if a TextView has a specific font size.

import android.view.View
import android.widget.TextView
import androidx.test.espresso.matcher.BoundedMatcher
import org.hamcrest.Description
import org.hamcrest.Matcher

fun withFontSize(size: Float): Matcher<View> {
return object : BoundedMatcher<View, TextView>(TextView::class.java) {
override fun describeTo(description: Description) {
description.appendText("has font size: $size")
}

override fun matchesSafely(textView: TextView): Boolean {
return textView.textSize == size
}
}
}

Usage in Test:

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId

class CustomFontSizeMatcherTest {
@Test
fun testTextViewWithFontSize() {
onView(withId(R.id.my_text_view))
.check(matches(withFontSize(16f)))
}
}

Example 3: Custom Matcher for Compound Drawable Position

Let’s create a more complex custom matcher that checks if a TextView has a drawable at a specific position (left, top, right, or bottom).

import android.graphics.drawable.Drawable
import android.view.View
import android.widget.TextView
import androidx.test.espresso.matcher.BoundedMatcher
import org.hamcrest.Description
import org.hamcrest.Matcher

fun withDrawableAtPosition(resourceId: Int, position: DrawablePosition): Matcher<View> {
return object : BoundedMatcher<View, TextView>(TextView::class.java) {
override fun describeTo(description: Description) {
description.appendText("has drawable resource $resourceId at position $position")
}

override fun matchesSafely(textView: TextView): Boolean {
val drawables = textView.compoundDrawables
val drawable = when (position) {
DrawablePosition.LEFT -> drawables[0]
DrawablePosition.TOP -> drawables[1]
DrawablePosition.RIGHT -> drawables[2]
DrawablePosition.BOTTOM -> drawables[3]
}
val expectedDrawable = textView.context.resources.getDrawable(resourceId, null)
return drawable?.constantState == expectedDrawable.constantState
}
}
}

enum class DrawablePosition {
LEFT, TOP, RIGHT, BOTTOM
}

Usage in Test:

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId

class CustomDrawablePositionMatcherTest {
@Test
fun testTextViewWithDrawableAtPosition() {
onView(withId(R.id.my_text_view))
.check(matches(withDrawableAtPosition(R.drawable.my_drawable, DrawablePosition.RIGHT)))
}
}

RecyclerViews are commonly used in Android applications, and you might need to create custom matchers to interact with items within a RecyclerView.

Example 4: Custom Matcher for RecyclerView Item Text

This custom matcher checks if a specific item in a RecyclerView has the desired text.

import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.matcher.BoundedMatcher
import org.hamcrest.Description
import org.hamcrest.Matcher

fun hasItemWithText(text: String): Matcher<View> {
return object : BoundedMatcher<View, RecyclerView>(RecyclerView::class.java) {
override fun describeTo(description: Description) {
description.appendText("has item with text: $text")
}

override fun matchesSafely(recyclerView: RecyclerView): Boolean {
val adapter = recyclerView.adapter ?: return false
for (i in 0 until adapter.itemCount) {
val holder = adapter.createViewHolder(recyclerView, adapter.getItemViewType(i))
adapter.bindViewHolder(holder, i)
val itemView = holder.itemView
if (itemView is TextView && itemView.text == text) {
return true
}
}
return false
}
}
}

Usage in Test:

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId

class RecyclerViewMatcherTest {
@Test
fun testRecyclerViewHasItemWithText() {
onView(withId(R.id.my_recycler_view))
.check(matches(hasItemWithText("Expected Text")))
}
}

Custom matchers for Toolbars can be useful to verify the toolbar title or menu items.

Example 5: Custom Matcher for Toolbar Title

This custom matcher checks if the toolbar has a specific title.

import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.test.espresso.matcher.BoundedMatcher
import org.hamcrest.Description
import org.hamcrest.Matcher

fun withToolbarTitle(title: CharSequence): Matcher<View> {
return object : BoundedMatcher<View, Toolbar>(Toolbar::class.java) {
override fun describeTo(description: Description) {
description.appendText("with toolbar title: $title")
}

override fun matchesSafely(toolbar: Toolbar): Boolean {
return toolbar.title == title
}
}
}

Usage in Test:

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId

class ToolbarMatcherTest {
@Test
fun testToolbarTitle() {
onView(withId(R.id.my_toolbar))
.check(matches(withToolbarTitle("Expected Title")))
}
}

Creating custom matchers in Android UI testing allows for more precise and flexible matching conditions. By extending the default matchers and implementing your own logic, you can handle complex UI scenarios more effectively. Try creating your own custom matchers to see how they can enhance your testing workflow.

Stay updated with the latest tips and best practices for QA engineers in Android UI testing by following us on Medium. For more resources and tools to enhance your testing skills, visit our website EspressoLab.ai.

Follow us on Medium: EspressoLab Medium

Thank you for reading! 🚀

Leave a Reply