Im AssertJ Projekt findet sich ein Generator, mit dem automatisch Assertions für vorgegebene Klassen generiert werden können. Damit wird dem Test Entwickler die eintönige Arbeit abgenommen, diese Assertions selbst zu schreiben. Ohne solche Assertions können die Möglichkeiten eines Framework, wie AssertJ oder Hamcrest nicht optimal genutzt werden.
Wer lieber mit Hamcrest arbeitet, muss auf solch ein Feature nicht zwangsläufig verzichten. Wie ein eigener Assertion Generator für Hamcrest erstellt werden kann, wird in diesem und weiteren Beiträgen erläutert.
Im folgenden ist ein einfacher JUnit Test dargestellt, der mit Hamcrest Matchern eine Person
Instanz prüft.
import static de.schegge.ancestor.PersonsMatcher.hasName; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.startsWith; import de.schegge.ancestor.Person; import de.schegge.processor.annotations.Hamcrest; import org.junit.jupiter.api.Test; @Hamcrest(Person.class) class PersonMatcherTest { @Test void test() { Person person = new Person(); person.setName("Jens Kaiser"); assertThat(person, hasName("Jens")); assertThat(person, not(hasName("Fiona"))); assertThat(person, hasName(startsWith("Kai"))); } }
Auffällig an dieser Test-Klasse ist die @Hamcrest
Annotation. Diese Annotation hat die Klasse Person
als Parameter, für die eigene Matcher benötigt werden.
@Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) public @interface Hamcrest { Class<?> value(); }
Die @Hamcrest
Annotation steht nur in den Sourcen zur Verfügung. Dies reicht aber für den hier vorgestellt Ansatz völlig aus.
Java bietet die Möglichkeit einen Annotation Processor zur Generierung zusätzlicher Dateien in die Compilierphase einzubauen. Solche Prozessoren werden u.a. von Javadoc, Immutables, MapStruct und Lombok genutzt.
Im obigen Beispiel ist in der Klasse schon ein Import für einen PersonenMatcher
zu sehen. Genau diese Klasse wird erzeugt, wenn die Test-Klasse mit der @Hamcrest
Annotation, das erste Mal übersetzt wird.
Die erzeugten Matcher-Klassen entsprechen immer den folgenden Beispiel.
public final class PersonMatchers { public static Matcher<Person> hasName(final String name) { return new FeatureMatcher<Person,String>(equalTo(name), "name", "name") { @Override protected String featureValueOf(final Person actual) { return actual.getName(); } }; } }
Der Name der Matcher-Klasse ergibt sich aus dem Namen der Ziel-Klasse und dem Suffix Matchers
. Für alle Getter der Ziel-Klasse wird eine statische Methode erzeugt, die einen
für dieses Attribut erzeugt.FeatureMapper
Der HamcrestMatcherBuilderProcessor
erzeugt Hamcrest Matcher Klassen und verarbeitet dafür @Hamcrest
Annotation. Dies wird mit der Annotation @SupportedAnnotationTypes
definiert.
@SupportedAnnotationTypes("de.schegge.processor.annotations.Hamcrest") @SupportedSourceVersion(SourceVersion.RELEASE_11) @AutoService(Processor.class) public class HamcrestMatcherBuilderProcessor extends AbstractProcessor { }
Nach der ersten Compilierungsphase und dem Aufsammeln vorhandener Annotationen, wird die process
Methode aller konfigurierten Prozessoren aufgerufen.
Damit dies auch für den eigenen Annotation Processor geschieht muss er in einer META-INF/service/javax.annotation.processing.Processor
Datei aufgeführt werden.
de.schegge.processor.impl.HamcrestMatcherBuilderProcessor
Im Fall des HamcrestMatcherBuilderProcessor
wird erst einmal geprüft, ob notwendige Properties definiert wurden. Wenn alles zu passen scheint werden die gefundenen Annotations
Elemente durchlaufen und die damit annotierten Typen bearbeitet.
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { if (properties.isEmpty()) { return false; } for (TypeElement annotation : annotations) { roundEnv.getElementsAnnotatedWith(annotation).stream() .map(TypeElement.class::cast).map(this::extractSourceClass) .filter(Objects::nonNull).distinct() .forEach(this::handleHamcrestAnnotation); } return true; }
Die obige Verarbeitung konnte einfach gehalten werden, da nur eine Annotation geprüft wird und diese nur Typen annotieren kann. Für komplexere Verarbeitungen bietet die Annotation Processor API übrigens eine eigene Visitor Implementierung.
Im oben dargestellten Stream wird aus dem TypeElement
in der Methode extractSourceClass
die benötigte Ziel-Klasse extrahiert. Da eine Klassen in mehreren @Hamcrest
Annotationen auftauchen kann, werden mit distinct
mögliche Dubletten aus dem Stream
entfernt.
Die eigentliche Arbeit geschieht in der handleHamcrestAnnotation
. Wie dort aus der Ziel-Klasse die eigentliche Matcher-Klasse generiert wird, ist der Inhalt des nächsten Beitrags zum Hamcrest Matcher Generator.
2 thoughts on “Hamcrest Matcher Generator”
Comments are closed.