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?
I've ended up solving this the following way for now. If someone has a more elegant solution, please tell!
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.)
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.)
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.