Using Espresso to test drawable changes

2019-03-15 08:08发布

问题:

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?

回答1:

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.



回答2:

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!



回答3:

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.



回答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("]");
        }
    }
}


回答5:

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?



回答6:

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