14

I'm new to Espresso testing, but there doesn't seem like there's any way to test drawable changes.

I have a tutorial that is an ImageView Drawable slideshow 'tucked into' a semi-transparent TextView. In my tests, I want to ensure that when the next button is pressed, the proper Drawable has been inserted into the tutorial's ImageView.

There is no default Matcher to check for Drawables, so I set out to write my own using https://stackoverflow.com/a/28785178/981242. Unfortunately, since there is no way to retrieve the id of an ImageView's active Drawable, I can't complete the matchesSafely() implementation.

This can't be the only use case for testing active Drawables. What is the tool that people normally use for situations like this?

Community
  • 1
  • 1
nukeforum
  • 1,214
  • 3
  • 15
  • 34

7 Answers7

14

I prefer not to compare bitmaps and instead follow this answer's advice: https://stackoverflow.com/a/14474954/1396068

When setting the image view's drawable, also store the drawable ID in its tag with setTag(R.drawable.your_drawable). Then use Espresso's withTagValue(equalTo(R.drawable.your_drawable)) matchers to check for the correct tag.

Maragues
  • 35,146
  • 14
  • 91
  • 94
Fabian Streitel
  • 2,220
  • 22
  • 35
  • Just to add the full withTagValue code: `withTagValue(CoreMatchers.equalTo(R.drawable.your_drawable))));`. Thanks. :) – Francislainy Campos Nov 12 '18 at 14:54
  • 1
    Theres just one problem with this solution - you need to add code in production that will be only used in tests and thats a big no-no. – Chapz Jul 26 '19 at 08:54
  • and you may remove the actual background and keep the tag, your UI test would still work and would not reflect the reality – JPhi Denis Apr 08 '21 at 07:46
10

please check this tutorial I found. Seems to work pretty good https://medium.com/@dbottillo/android-ui-test-espresso-matcher-for-imageview-1a28c832626f#.4snjg8frw

Here is the summary for copy pasta ;-)

public class DrawableMatcher extends TypeSafeMatcher<View> {

    private final int expectedId;
    String resourceName;

    public DrawableMatcher(int expectedId) {
        super(View.class);
        this.expectedId = expectedId;
    }

    @Override
    protected boolean matchesSafely(View target) {
        if (!(target instanceof ImageView)){
            return false;
        }
        ImageView imageView = (ImageView) target;
        if (expectedId < 0){
            return imageView.getDrawable() == null;
        }
        Resources resources = target.getContext().getResources();
        Drawable expectedDrawable = resources.getDrawable(expectedId);
        resourceName = resources.getResourceEntryName(expectedId);

        if (expectedDrawable == null) {
            return false;
        }

        Bitmap bitmap = ((BitmapDrawable) imageView.getDrawable()).getBitmap();
        Bitmap otherBitmap = ((BitmapDrawable) expectedDrawable).getBitmap();
        return bitmap.sameAs(otherBitmap);
    }


    @Override
    public void describeTo(Description description) {
        description.appendText("with drawable from resource id: ");
        description.appendValue(expectedId);
        if (resourceName != null) {
            description.appendText("[");
            description.appendText(resourceName);
            description.appendText("]");
        }
    }
}

Please be aware that this only works when your Drawable is a BitmapDrawable. If you also have VectorDrawable or other Drawable you have to check for this (imageView.getDrawable() instanceOf XXXDrawable) and get the bitmap out of it. Except you have some kind of simple Drawable where you just have one color or so you can compare.

To get the Bitmap of a VectorDrawable for example you have to draw the VectorDrawable to a canvas and save it to a bitmap (I had some trouble when the VectorDrawable was tinted). If you have a StateListDrawable you can get the Drawable of the selected state and repeat your if instanceOf cascade. For other Drawable types I don't have any experience, sorry!

wolle
  • 135
  • 1
  • 9
  • 1
    This will not work if your `Drawable` is not `BitmapDrawable` which may be `GradientDrawable`, `ColorDrawable` or `ShapeDrawable` and many others – riwnodennyk Jun 15 '16 at 20:58
  • For comparing Vector Drawable, a simpler way is to use getConstantState(). Refer to http://stackoverflow.com/questions/9125229/comparing-two-drawables-in-android. I have tested it on simple vector drawables (from the material design icons collection), and it works well for them. – FreewheelNat Jan 06 '17 at 11:40
8

There is one gist which contains withBackground(), withCompoundDrawable(), withImageDrawable() matchers from Frankie Sardo. All credits to him.

And regarding image ids - you can type R.drawable.image_name, then the id of the drawable will be retrieved automatically.

denys
  • 6,366
  • 3
  • 32
  • 34
4

Based on @wolle and @FreewheelNat's help, for comparing (Vector) Drawable:

public static Matcher<View> withDrawableId(@DrawableRes final int id) {
    return new DrawableMatcher(id);
}


public static class DrawableMatcher extends TypeSafeMatcher<View> {

    private final int expectedId;
    private String resourceName;

    public DrawableMatcher(@DrawableRes int expectedId) {
        super(View.class);
        this.expectedId = expectedId;
    }

    @Override
    protected boolean matchesSafely(View target) {
        if (!(target instanceof ImageView)) {
            return false;
        }
        ImageView imageView = (ImageView) target;
        if (expectedId < 0) {
            return imageView.getDrawable() == null;
        }
        Resources resources = target.getContext().getResources();
        Drawable expectedDrawable = resources.getDrawable(expectedId);
        resourceName = resources.getResourceEntryName(expectedId);
        if (expectedDrawable != null && expectedDrawable.getConstantState() != null) {
            return expectedDrawable.getConstantState().equals(
                    imageView.getDrawable().getConstantState()
            );
        } else {
            return false;
        }
    }


    @Override
    public void describeTo(Description description) {
        description.appendText("with drawable from resource id: ");
        description.appendValue(expectedId);
        if (resourceName != null) {
            description.appendText("[");
            description.appendText(resourceName);
            description.appendText("]");
        }
    }
}
drakeet
  • 2,359
  • 1
  • 19
  • 28
1

I accept the answer of @wolle as valid, but I would like to admit that, even for Java, it could be even simpler than that. It can be converted into a static function (or a companion in Kotlin) and also clean some deprecated code.

Anyway, the code-compacted-non-deprecated solution for Kotlin would be this:

    fun drawableIsCorrect(@DrawableRes drawableResId: Int): Matcher<View> {
        return object : TypeSafeMatcher<View>() {
            override fun describeTo(description: Description) {
                description.appendText("with drawable from resource id: ")
                description.appendValue(drawableResId)
            }

            override fun matchesSafely(target: View?): Boolean {
                if (target !is ImageView) {
                    return false
                }
                if (drawableResId < 0) {
                    return target.drawable == null
                }
                val expectedDrawable = ContextCompat.getDrawable(target.context, drawableResId)
                        ?: return false

                val bitmap = (target.drawable as BitmapDrawable).bitmap
                val otherBitmap = (expectedDrawable as BitmapDrawable).bitmap
                return bitmap.sameAs(otherBitmap)
            }
        }
    }

22 lines vs 44, eh?

Rafael Ruiz Muñoz
  • 4,769
  • 6
  • 41
  • 79
1

Here's a Kotlin version of @drakeet's answer with a few modifications.

class DrawableMatcher(private val targetContext: Context,
                  @param:DrawableRes private val expectedId: Int) : TypeSafeMatcher<View>(View::class.java) {

override fun matchesSafely(target: View): Boolean {
    val drawable: Drawable? = when(target) {
        is ActionMenuItemView -> target.itemData.icon
        is ImageView -> target.drawable
        else -> null
    }
    requireNotNull(drawable)
    
    val resources: Resources = target.context.resources
    val expectedDrawable: Drawable? = resources.getDrawable(expectedId, targetContext.theme)
    return expectedDrawable?.constantState?.let { it == drawable.constantState } ?: false
}

override fun describeTo(description: Description) {
    description.appendText("with drawable from resource id: $expectedId")
    targetContext.resources.getResourceEntryName(expectedId)?.let { description.appendText("[$it]") }
}
}
Taslim Oseni
  • 4,610
  • 10
  • 34
  • 50
John A Qualls
  • 539
  • 1
  • 3
  • 15
0

I already answered on the similar topic here: Get the ID of a drawable in ImageView. The approach is based on tagging a view with a specified resource id in the custom LayoutInflater. Whole process is automated by a simple library TagView. It's especially handy for Espresso test because you don't need to tag every view in your project manually. In fact, you don't need to change anything, except you set some drawables in runtime. In that case you need to look into Tagging in runtime section.

As a result, you can compare two drawables just by their ids:

onView(withId(R.id.imageview)).check(assertTagKeyValue(
               ViewTag.IMAGEVIEW_SRC.id, android.R.drawable.ic_media_play));

Custom Espresso assertion assertTagKeyValue is available here

Bogdan Kornev
  • 71
  • 1
  • 4