Ein Hamcrest Entity Matcher
Der hier vorgestellte Hamcrest Matcher entstand im Rahmen eines Projektes, in dem sehr viele Java Entity Objekte auf Korrektheit ihrer Properties getestet werden mussten. Mit dem Matcher können mittels eines einzelnen Assert-Statement alle Properties einer Klasse auf Korrektheit geprüft werden. Dabei werden alle fehlgeschlagenen Validierungen am Ende der Prüfung in einer übersichtlichen Fehlermeldung dargestellt.
Zu Beginn ein Beispiel
Nach Ausführung des Tests werden alle Property-Abweichungen angezeigt:
java.lang.AssertionError:
Expected: a entity with specified property values
but: got entity with 2 invalid values [
-->age (expected:42, actual:7),
-->lastName (expected:Maier, actual:Mayer)]
Setup
Zur Demonstration des Matchers wird die Klasse Person verwendet:
Motivation
Es gibt schon einige Möglichkeiten zum Testen von Properties mit Junit und Hamcrest. Allerdings hat jeder dieser Ansätze Nachteile für unser Einsatzszenario, die wir uns einmal anschauen. Daher haben wir einen neuen Entity Manager entwickelt.
Naiver Ansatz zum Testen vieler Properties einer Instanz
Der naive Ansatz für das geschilderte Problem verwendet pro Property ein einzelnes JUnit assertEquals-Statement:
Wie zu erwarten schlägt der Test fehl mit der folgenden Meldung:
org.junit.ComparisonFailure: lastname correct
Expected :Maier
Actual :Mayer
<Click to see difference>
at org.junit.Assert.assertEquals(Assert.java:115)
at com.opitzconsulting.entitymatcher.NaiverAnsatz.testing_single_properties_without_hamcrest(NaiverAnsatz.java:13)
Nachteile dieses Ansatzes:
-
Es ist sehr aufwändig, für jedes einzelne Property ein eigenes assert-Statement zu Schreiben.
-
Werden die gleichen Prüfungen in verschiedenen Test-Methoden benötigt, müssen die Statements immer kopiert werden. Es entsteht also schlecht wartbarer Code.
-
Die Überprüfung der assert-Statements hört bei dem ersten Fehlschlag auf. So wird bei Ausführung des Beispiel-Tests nicht angezeigt, dass auch das Alter der Person nicht korrekt ist.
Verwendung der JUnit ErrorCollector-Rule
In diesem Ansatz verwenden wir nicht einzelne assertEquals-Statements, sondern den ErrorCollector aus dem JUnit Framework. Dazu muss in der Testklasse eine Instanz der Klasse
org.junit.ErrorCollector
angelegt und mit der Annotation @Rule versehen werden:
Vorteile:
-
Im Gegensatz zu dem ersten Ansatz bricht die Testausführung nach dem ersten fehlgeschlagenen Assert-Statement nicht ab. Stattdessen wird der Test bis zum Ende ausgeführt. Anschließend wird der ErrorCollector ausgewertet und die fehlgeschlagen Prüfungen werden protokolliert:
java.lang.AssertionError: lastname correct Expected: "Maier" but: was "Mayer" at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20) java.lang.AssertionError: age correct Expected: <42> but: was <7> at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
Leider sind zwei Probleme des ersten Ansatztes auch hier noch nicht gelöst:
-
Der Code ist immer noch sehr aufwändig, weil pro Property ein Statement geschrieben werden muss.
-
Die Wartbarkeit ist immer noch nicht gut.
Verwendung von Hamcrest samePropertyValuesAs
Durch die Verwendung des Matchers samePropertyValuesAs kommen wir schon recht Nahe an unser Ziel, einfache Assert-Statements zu schreiben:
Der Test schlägt mit der folgenden Meldung fehl:
java.lang.AssertionError:
Expected: same property values as Person [age: <42>, email: null, firstName: "Hans", lastName: "Maier"]
but: age was <7>
Vorteile:
- Die Wartbarkeit ist nun deutlich erhöht, da nur noch ein einzelnes Assert-Statement geschrieben werden muss.
Nachteile:
- Leider stoppt auch der samePropertyValuesAs - Matcher die Ausführung, sobald ein invalides Property gefunden wurde. Die Abweichung in dem Property lastName wurde nicht protokolliert.
Entity Matcher
Der hier vorgestellte Entity Matcher erfüllt alle drei genannten Anforderungen. Beispiel-Aufruf:
Nach Ausführung des Tests werden alle Property-Abweichungen angezeigt:
java.lang.AssertionError:
Expected: a entity with specified property values
but: got entity with 2 invalid values [
-->age (expected:42, actual:7),
-->lastName (expected:Maier, actual:Mayer)]
Vorteile:
- Genau wie beim Hamcrest Matcher “samePropertyValuesAs” wir nur ein einzelnes assert-Statement benötigt.
- Der Entity Matcher überprüft alle Properties, alle Validierungsfehler werden am Ende protokolliert.
- Weiterhin sind mit dem Entity Matcher noch einige andere Dinge möglich, die in den folgenden Abschnitten dargestellt werden.
Feature: Prüfe nur definierte Properties
Mittels der Methode matchesSpecifiedProperties werden nur die Properties einer Klasse geprüft, die beim Aufruf spezifiziert werden
Im folgenden Test wird zur Demontration im ersten assertThat-Statement validiert, dass der Nachname und das Alter gleich sind. In dem zweiten assertThat-Statement wird validiert, dass der Vorname nicht gleich ist.
Feature: Prüfe alle mit Ausnahme der definierten Properties
Mittels der Methode matchesAllPropertiesExcluding werden alle Properties einer Klasse mit Ausnahme der spezifizierten Properties geprüft.
Im folgenden Test wird zur Demonstration validiert, dass alle Properties mit Ausnahme von Vorname und E-Mail-Adresse gleich sind:
Download des Entity-Matchers
Der Entity-Matcher ist auf Github verfügbar: entity-matcher