Search code examples
androidandroid-espressoandroid-testing

Espresso onData with check (assertion) on data object instead of View


I have a ListAdapter for objects of type Foo. These objects have fields int id and double quantity.

In my Espresso tests, I would like to assert that the data entry with id = 5 has quantity = 10.

(Assume that the methods FooMatcher.withId(int) and FooMatcher.withQuantity(double) exist and do what you would think they do.)

I could rely on how the adapter renders this visually, like so I believe:

onData(FooMatcher.withId(5))
  .onChildView(withId(R.id.quantityTextView))
  .check(matches(withText("10")));

(I haven't actually tested this but I guess it would work if the double value 10 is rendered as "10" inside a view with id quantityTextView.)

However, I don't want to rely on how the double value is rendered, which depends on localization and configuration options. It would be much more elegant if I could match the data directly, like so:

onData(FooMatcher.withId(5))
  .checkData(matches(FooMatcher.withQuantity(10)));

(This is pseudo-code; .checkData() doesn't exist. You can call .check() on the DataInteraction returned by onData() but it expects a ViewAssertion so if you use matches(...) it expects a Matcher<? extends View> whereas I want to use a Matcher<Foo>.)

Is there an elegant way to achieve something to this effect?


Solution

  • I've ended up solving this the following way for now. If someone has a more elegant solution, please tell!

    Prerequisites

    First of all, I'm using a View subclass called ListItemView for the views rendered by my ListAdapter. (I.e., the getView() implementation of my ListAdapter returns instances of ListItemView.)

    I've added a field public Foo item; to my ListItemView class, and populate that field with a reference to the data object being rendered in the getView() implementation of my ListAdapter.

    (This probably violates one or another principle of the Model-View-Controller pattern but I don't see any actual downsides to doing this in my codebase.)

    Basic idea

    Now, since the view object has a reference to the data object, I can write a ViewAssertion that actually performs tests on the data object, like so:

    onData(FooMatcher.withId(5)).check(matches(new TypeSafeMatcher<>() {
        @Override
        protected boolean matchesSafely(View view) {
            if (view instanceof ListItemView v && v.item instanceof Foo foo) {
                return foo.quantity == 10;
            }
            return false;
        }
    
        @Override
        public void describeTo(Description description) {
        }
    }));
    

    (Note that this code uses Java 17 pattern variables, which is supported by the newest Android SDK.)

    Making it pretty

    Of course, that's incredibly ugly, but we can make it pretty with a few helpers. Firstly, I already have the following subclass of TypeSafeMatcher to make it a little bit nicer to create ad-hoc Matchers using Lambda expressions:

    package org.example.myapp.tests.util;
    
    import org.hamcrest.Description;
    import org.hamcrest.TypeSafeMatcher;
    
    import java.util.function.Predicate;
    
    class PredicateMatcher<T> extends TypeSafeMatcher<T> {
        private final Predicate<T> predicate;
        private final String description;
    
        PredicateMatcher(Predicate<T> predicate, String description) {
            this.predicate = predicate;
            this.description = description;
        }
    
        @Override
        protected boolean matchesSafely(T item) {
            return predicate.test(item);
        }
    
        @Override
        public void describeTo(Description description) {
            description.appendText(this.description);
        }
    }
    

    In addition to that, I've now added a utility class called Check which allows for quick and easy creation of ViewAssertion instances :

    package org.example.myapp.tests.util;
    
    import androidx.test.espresso.*;
    import androidx.test.espresso.assertion.*;
    
    import java.util.function.Predicate;
    
    import org.example.myapp.model.Foo;
    
    public class Check {
        // Generic checks on Foo objects using a Predicate<Foo>
        public static ViewAssertion foo(String description, Predicate<Foo> check) {
            return ViewAssertions.matches(new PredicateMatcher<>(description, view -> {
                if (view instanceof ListItemView v && v.item instanceof Foo foo) {
                    return check.test(foo);
                } else {
                    throw new RuntimeException("Type mismatch.");
                }
            }));
        }
    
        // Check that a Foo has a given quantity.
        public static ViewAssertion fooQuantity(double quantity) {
            return foo("quantity is " + quantity, f -> f.quantity == quantity);
        }
    }
    

    Now my test codes looks very pretty:

    onData(FooMatcher.withId(5)).check(Check.fooQuantity(10));
    
    // Use generic check method for properties that don't have a direct check:
    onData(FooMatcher.withId(6)).check(Check.foo(
        "rarelyCheckedProperty is someValue",
        f -> f.rarelyCheckedProperty == someValue
    ));
    

    And everyone lived happily ever after.