In jedem Projekt entwickelt sich nach einiger Zeit ein eigenes Ökosystem von Klassen, die sich um die Bereitstellung von Testdaten für Unit Tests und Integrationstests kümmern. Manchmal bleibt dieses Ökosystem klein und übersichtlich, häufig jedoch wuchert es zu einem undurchschaubaren Dschungel an.
JUnit 5 bietet mit seiner Extension Model einen leichtgewichtigen Ansatz um Testdaten direkt in die jeweilige Testmethode zu injizieren. Im Beitrag Personas für Unit Tests wurde gezeigt, wie mit dem ParameterResolver
und dem InvocationInterceptor
Testdaten vor der Testausführung generiert und manipuliert werden können. Ausgeblendet wurde dabei die Zuweisung der Persona Eigenschaften auf die Testobjekte im FakePersonaBuilder
.
Mit Personas und ihren Eigenschaften sind in diesem Beitrag standardisierte Personendaten gemeint. Jede Persona besitzt einen festen Satz von Eigenschaften. Dies sind Geschlecht, Anrede, Titel, Vorname, Nachname, Telefonnummer, Login, Email-Adresse, Straße, Hausnummer, PLZ und Wohnort.
Bei der Zuordnung von Persona Eigenschaften zu den Attributen einer Java Klasse gibt es das Problem der Benamung. In unterschiedlichen Klassen, Bibliotheken, Projekten oder Firmen werden diese Attribute unterschiedlich benannt. Beispielsweise kann der Nachname in einer Bean als surname
, surName
, lastname
, lastName
, lname
, sname
oder nachname
deklariert werden. Damit der FakePersonaBuilder
diese Attribute zuordnen kann, werden die üblichen Attributnamen in einer Alias-Liste hinterlegt.
private static final Map<String, PersonaAttribute> ALIASES = Map.ofEntries( Map.entry("gender", GENDER), Map.entry("sex", GENDER), Map.entry("title", TITLE), Map.entry("salutation", SALUTATION), Map.entry("firstname", FIRSTNAME), Map.entry("givenname", FIRSTNAME), Map.entry("lastname", LASTNAME), Map.entry("surname", LASTNAME), Map.entry("email", EMAIL), Map.entry("mail", EMAIL), Map.entry("telephone", PHONE), Map.entry("phone", PHONE), Map.entry("street", STREET), Map.entry("housenumber", HOUSENUMBER), Map.entry("number", HOUSENUMBER), Map.entry("zip", ZIP), Map.entry("city", CITY) );
Der Schlüssel in der Map
ist der Attributname in der Bean und der Wert ist eine Enum Konstante für die jeweilige Persona Eigenschaft. Ein Attributname wird unterstützt, wenn ein entsprechender Eintrag case insensitive in der Map
zu finden ist. Für manche Fälle ist diese Map
ungeeignet, daher ist sie nur die Basis der tatsächlich verwendeten Map
. Die tatsächlich verwendete Map
kann über entsprechende Methoden um weitere Einträge ergänzt oder von bestehenden Einträgen befreit werden.
Die Personas sind in einer JSON Struktur hinterlegt. Dabei beinhalten die Eigenschaften keine Einzelwerte sondern Listen von Werten. Enthält die Liste mehrere Werte, dann wird der tatsächlich verwendete Wert gewürfelt. Auf diese Weise können die Testdaten für eine Persona im Tests variiert werden.
{ "mitarbeiterin": { "gender": [ "female" ], "salutation": [ "Frau" ], "title": [], "firstname": [ "Andrea", "Bettina", "Claudia" ], "lastname": [ "Mustermann" ], "email": [ "mustermann@fakenews.de" ], "street": [ "Badstraße", "Turmstraße", "Südbahnhof", "Chauseestraße", "Elisenstraße", "Poststraße" ], "housenumber": [ "1", "2a", "3b", "4", "5c", "6", "7d", "8", "9", "10" ], "zip": [ "33334", "33619", "32257" ], "city": [ "Gütersloh", "Bielefeld", "Bünde" ] } "adam": { "gender": [ "male" ], "salutation": [ "Herr" ], "title": [ "Professor" ], "firstname": [ "Johann Adam" ], "lastname": [ "Weißhaupt" ], "street": [], "housenumber": [], "zip": [], "city": [ "Ingolstadt" ] } }
Die Persona mitarbeiterin
aus der JSON Struktur besitzt immer die Anrede Frau
, da nur ein einzelner Wert hinterlegt ist. Als Straße besitzt die Persona immer eine Straße von der ersten Seite eines Monopoly Spiels.
Der FakePersonaBuilder
verwendet eine Fluent API, damit ist der Aufruf und die Konfiguration in einem Statement möglich.
new FakePersonaBuilder().build("mitarbeiterin", person);
In diesem Beispiel wird der FakePersonaBuilder
ohne weitere Konfiguration aufgerufen um die Attribute von person
mit den Eigenschaften der Persona mitarbeiterin
zu belegen. Komplexere Ausdrücke, mit verschiedenen Konfigurationsmöglichkeiten, sind aber auch möglich.
new FakePersonaBuilder() .ignore(PersonaAttribute.EMAIL) .override(PersonaAttribute.TITLE) .enable(Feature.NULL_ON_MISSING_ATTRIBUTE) .alias("nachname", PersonaAttribute.LASTNAME) .build("mitarbeiterin", person);
Der Großteil der Implementierung befindet sich in der build
Methode. Um die Daten aus der Bean zu erhalten und die veränderten Daten wieder in die Bean einzufügen, bedienen wir uns eines alten Bekanten au den Beiträgen Dr. REST oder: Wie ich lernte Jackson zu lieben und Trivial Pursuit – API MarkDown.
public <T> T build(String personaName, T bean) { requireNonNull(personaName); requireNonNull(bean); try { Map<String, Object> map = objectMapper.readValue(objectMapper.writeValueAsString(bean), new TypeReference<>() { }); Map<String, Object> filled = fillPersona(personaName, map); objectMapper.readerForUpdating(bean).readValue(objectMapper.writeValueAsString(filled)); } catch (JsonProcessingException e) { throw new ConversionException("cannot fill with personaName " + personaName, e); } return bean; }
Am Anfang der Methode wird der ObjectMapper
aus der Jackson Bibliothek verwendet um die Werte aus der Bean in eine Map
zu kopieren. Dann werden die Persona Eigenschaften in die Map
kopiert. Als letztes wird die Map
mit Hilfe der readerForUpdating
Methode in die Bean zurückgeschrieben.
Für den Unit Tests illuminatus
aus dem vorherigen Beitrag mit der Persona adam
geschieht das folgende hinter den Kulissen.
@ExtendWith(PersonParameterResolver.class) @ExtendWith(IdInvocationInterceptor.class) @ExtendWith(FakeInvocationInterceptor.class) class PersonaTest { @Test void illuminatus(@Id(23) @Fake("adam") PersonEntity person) { ... }
Zuerst wird die als Parameter übergebene PersonEntity
in eine Map
umgewandelt. Zuvor hat der PersonParameterResolver
eine leere Instanz erzeugt und der IdInvocationInterceptor
hat das id
Feld mit dem Wert 23
belegt.
{ "id":23, "gender":null, "firstname":null,"lastname":null, "street":null,"housenumber":null, "zip":0,"city":null, }
In der JSON Darstellung sind alle Felder bis auf id
mit null
belegt. Befüllt mit den Werten für die Persona adam
, ändern sich die Einträge gender
, firstname
, lastname
und city
.
{ "id":1, "gender":"male", "firstname":"Johann Adam", "lastname":"Weißhaupt", "street":null,"housenumber":null, "zip":0, "city": "Ingolstadt" }
Nachdem die Map
zurück in die PersonEntity
geschrieben ist, enthält diese alle notwendigen Personendaten für den illuminatus
Test.
Der Sourcecode für diesen Beitrag ist im Gitlab Projekt Fake Personas zu finden. Viel Spaß beim Testen mit Personas.