Unscharfe Hamcrest-Matcher für Elemente in Collections

Hamcrest bietet von Haus aus eine breite Palette von Matchern für verschiedenste Situationen an.

Um zu prüfen, ob ein Elemente oder auch mehrere in einer Collection sind, gibt es z.B. contains() und containsInAnyOrder() sowie hasItem() und hasItems(). Alle vier funktionieren sehr gut, solange man sich auf die mitgelieferten Matcher von Hamcrest beschränkt.

Sobald man aber Objekte unscharf prüfen will, also nicht mit equals(), sondern sich auf einige Attribute beschränken möchte, muss man etwas außenrum denken.

Die Klasse Element steht stellvertretend für die Objekte, die geprüft werden sollen. Im Beispiel sollen nur die Attribute Name und Value geprüft werden. Natürlich kann die Klasse mehr Attribute haben und es könnten auch mehr Attribute getestet werden.

public class Element {

    private final String name;
    private final String value;
    // more private members
    
    public Element(final String pName, final String pValue) {
        this.name = pName;
        this.value = pValue;
    }
    
    public String getName() {
        return this.name;
    }
    
    public String getValue() {
        return this.value;
    }

    // more getters
}

Für die Prüfung dieser Attribute wird ein neuer, einfacher Matcher erstellt.
Der Matcher hat zwei Factory-Methoden. Die erste liefert einen Matcher, der nur das Attribut Name testet (Zeile 7), die zweite liefert einen Matcher, der sowohl Name als auch Value prüft (Zeile 12).
Die eigentliche Prüfung wird in der methode matchesSafely() (Zeile 17) durchgeführt.

public class ElementMatcher extends TypeSafeMatcher<Element> {

    private final String expectedName;
    private final String expectedValue;

    @Factory
    public static ElementMatcher elementIs(final String pName, final String pValue) {
        return new ElementMatcher(pName, pValue);
    }

    @Factory
    public static ElementMatcher elementIs(final String pName) {
        return new ElementMatcher(pName);
    }

    @Override
    protected boolean matchesSafely(final Element pElement) {
        return
            this.expectedName.equals(pElement.getName()) &&
            (null == this.expectedValue || this.expectedValue.equals(pElement.getValue()));
    }

    private ElementMatcher(final String pName, final String pValue) {
        this.expectedName = pName;
        this.expectedValue = pValue;
    }

    private ElementMatcher(final String pName) {
        this(pName, null);
    }

    // additional matcher methods
}

Für den Matcher kann ein einfacher Test erstellt werden:

    @Test
    public void testElementMatcher() {
        Element e = new Element("e", "test");
        assertThat(e, elementIs("e"));
    }

Läuft der Test, kann das Prinzip auf eine Collection angewendet werden.
Die Collection für den Test wird in Zeile 3 erstellt, die Matcher für die einzelnen Elemente auf Zeile 7.
Angewendet werden können die Matcher mit einer Variante des Hamcrest Matchers IsCollectionContaining, nämlich hasItems(). Diese Methode erwartet ein Array vom Typ auf den die Matcher spezialisiert wurden als Parameter.

    @Test
    public void testListContainsElement() {
        List<Element> elementList = new ArrayList<Element>();
        elementList.add(new Element("el1", "test1"));
        elementList.add(new Element("el2", "test2"));

        List<ElementMatcher> elementMatchers = new ArrayList<ElementMatcher>();
        elementMatchers.add(elementIs("el1"));
        elementMatchers.add(elementIs("el2", "test2"));
        assertThat(elementList, hasItems(elementMatchers.toArray( new ElementMatcher[0] )));
    }

Die Methode hasItems(), die Hamcrest mitliefert, kann auf Grund von Grenzen der Generics nur mit Arrays umgehen, nicht mit Collections. Das ist lästig und liest sich nicht schön. Eine Implementierung, die Collections als Parameter akzeptiert, hätte die folgende Notation.

public static <T> Matcher<Iterable<T>> hasItems(Iterable<Matcher<T>> elementMatchers)

Abgesehen davon, dass die Verwendung dieser Methode nicht wirklich schöner aussähe, als die originale Hamcrest-Variante, kann man sie nicht elegant an die originalen Hamcrest-Methode hasItems() delegieren.

assertThat(elementList, ExtendedIsCollectionContaining.<Element>hasItems(matchers));

Da nichts dagegen spricht, den ElementMatcher mit einer Prüfung von Collections zu versehen, wird die Klasse um folgende Methode ergänzt:

    @Factory
    public static Matcher<Iterable<Element>> hasItems(final Collection<ElementMatcher> elementMatchers) {
        return IsCollectionContaining.hasItems(elementMatchers.toArray( new ElementMatcher[0] ));
    }

Damit wird die Implementierung der Methode testListContainsElement() deutlich schöner lesbar und entspricht eher dem sonst üblichen Hamcrest-Stil (Zeile 10).

    @Test
    public void testListContainsElement() {
        List<Element> elementList = new ArrayList<Element>();
        elementList.add(new Element("el1", "test1"));
        elementList.add(new Element("el2", "test2"));

        List<ElementMatcher> elementMatchers = new ArrayList<ElementMatcher>();
        elementMatchers.add(elementIs("el1"));
        elementMatchers.add(elementIs("el2", "test2"));
        assertThat(elementList, hasItems(elementMatchers));
    }

Wo nützt einem das Ganze mit dem „unscharfen“ Matchern?
Ich habe es im Kontext von JPA Entities benötigt, die in den Testfällen detached oder attached sein konnten und somit nicht via equals() geprüft werden konnten. Da für den Test nur einige übereinstimmenden Attribute relevant waren, bin ich schlußendlich zu dieser Lösung gekommen.
Der Quellcode steht in meinem Examples-Projekt auf Github.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.