ISENTO.MAGAZIN

WireMock erweitern: Eigene Response Transformer entwickeln und als Docker-Container bereitstellen

Im ersten Teil dieser Serie wurde WireMock als vielseitiger Mockserver vorgestellt. Durch eine grundlegende Einrichtung und anhand von Beispielen wurde gezeigt, wie WireMock für gängige Testfälle effektiv eingesetzt werden kann. 

Dieser zweite Teil widmet sich nun der Erweiterung von WireMock durch eigene Implementierungen und seiner effizienten Bereitstellung als Container-Image. 

Ein typisches Szenario, das die Notwendigkeit solcher Erweiterungen verdeutlicht, ist die Simulation komplexer Interaktionen – beispielsweise, wenn ein Service auf einer PaaS-Plattform mit einem externen Server kommuniziert, dessen Antworten komprimierte und kodierte Daten enthalten. Hierfür stößt das standardmäßige Response Templating schnell an seine Grenzen. 

Besonders vorteilhaft ist in solchen Fällen die Möglichkeit, alle benötigten Dateien und Abhängigkeiten in einer einzigen Fat JAR zu bündeln und dieses Artefakt dann direkt in einem Container auszuführen. Dies vereinfacht das Deployment und die Verwaltung.

Was ist eine Fat JAR?

Eine Fat JAR (auch Uber JAR genannt) ist eine ausführbare Java-Datei, die neben dem eigenen Programmcode sämtliche benötigten Bibliotheken und Ressourcen enthält. Dadurch lässt sich eine Anwendung ohne zusätzliche Abhängigkeiten starten und besonders einfach verteilen oder containerisieren.

Im Folgenden wird Schritt für Schritt gezeigt, wie dies umgesetzt wird: Zuerst wird die Erstellung einer Fat JAR mit der WireMock Standalone-Version erläutert. Danach wird anhand eines praktischen Beispiels demonstriert, wie eine benutzerdefinierte Transformer-Klasse definiert und implementiert wird. Abschließend wird gezeigt, wie das fertige Fat JAR erfolgreich in einem Docker-Container ausgeführt werden kann. 

WireMock als Fat JAR bereitstellen

1. Maven-Projekt erstellen: Zuerst wird ein neues Maven-Projekt erstellt.

2. Dependency hinzufügen: Anschließend wird die Abhängigkeit wiremock-standalone in die pom.xml eingefügt: 

				
					<dependency> 
    <groupId>org.wiremock</groupId> 
    <artifactId>wiremock-standalone</artifactId> 
    <version>3.13.2</version> 
</dependency> 
				
			

3. Maven-Shade-Plugin konfigurierenUm ein ausführbares Fat JAR zu erstellen, wird das mavenshade-plugin in die pom.xml eingefügt. Dieses Plugin bündelt alle Abhängigkeiten (einschließlich wiremock-standalone) sowie alle benötigten Ressourcen (z. B. die WireMock-Stubs und Dateien) zu einer einzigen, direkt ausführbaren JAR-Datei:

				
					<plugin> 
    <groupId>org.apache.maven.plugins</groupId> 
    <artifactId>maven-shade-plugin</artifactId> 
    <version>3.6.2</version> 
    <executions> 
        <execution> 
            <phase>package</phase> 
            <goals> 
                <goal>shade</goal> 
            </goals> 
            <configuration> 
                <!-- Eigene Manifest-Dateien ausschließen --> 
                <filters> 
                    <filter> 
                        <artifact>org.isento:wiremock-demo</artifact> 
                        <excludes> 
                            <exclude>META-INF/MANIFEST.MF</exclude> 
                        </excludes> 
                    </filter> 
                    <filter> 
                        <artifact>*:*</artifact> 
                        <excludes> 
                            <exclude>META-INF/*.SF</exclude> 
                            <exclude>META-INF/*.DSA</exclude> 
                            <exclude>META-INF/*.RSA</exclude> 
                        </excludes> 
                    </filter> 
                </filters> 
                <outputFile>./target/wiremock-demo-bundled.jar</outputFile> 
            </configuration> 
        </execution> 
    </executions> 
</plugin> 
				
			

4. Projekt bauenmvn package aufrufen, um das Projekt zu kompilieren.

5. Fat JAR erstellen und starten: Das generierte Fat JAR (im Verzeichnis target) lässt sich lokal mit java -jar wiremock-demo-bundled.jar starten. 

Projektstruktur 

Die zwei zentralen Ordner für die Konfiguration (mappings und __files) sollten nicht direkt im Root von src/main/resources platziert werden, sondern in einem Unterordner (z. B. src/main/resources/wiremock/). Andernfalls kann WireMock die Dateien nicht aus dem Classpath laden, siehe Wiremock Doku. 

Die Projektstruktur könnte folgendermaßen aussehen: 

  • src/main/resources/wiremock/__files 
  • src/main/resources/wiremock/mappings

Build & Ausführung 

Beim Build (mvn package) wird ein Fat JAR mit den Ordnern mappings/ und __files/ im Root erstellt. Das JAR wird mit folgendem Befehl aufgerufen, wobei wiremock der Überordner in unserem Beispiel ist: 

				
					java -jar ./target/wiremock-demo-bundled.jar --load-resources-from-classpath wiremock 
				
			

Ergebnis: 

Wird eine Anfrage an die konfigurierte URL gesendet, gibt WireMock die definierte Antwort zurück: 

				
					GET localhost:8080/users 

[ 
  { 
    "name": "Julia Schneider",
    "age": 28, 
    "location": "Brussels", 
    "profilePicture": "" 
  }, 
  { 
    "name": "Markus Weber", 
    "age": 41, 
    "location": "Berlin", 
    "profilePicture": "" 
  } 
] 
				
			

Benutzerdefinierte Response-Transformer 

Neben dem response-template lassen sich auch eigene Transformer erstellen. Dazu wird das Interface ResponseTransformerV2 (abgeleitet von Extension) implementiert. Drei Methoden sind entscheidend: 

  • transform: implementiert die Logik zur Manipulation der Antwort (z. B. zur Anpassung von Body oder Headern). 
  • applyGlobally: gibt an, ob der Transformer global (für alle Anfragen) oder nur für spezifische Stubs angewendet wird. 
  • getName: gibt den eindeutigen Namen des Transformers zurück. Dieser Name wird verwendet, um den Transformer in der Stub-Konfiguration zu referenzieren. 

Was ist ein Response Transformer in WireMock?

Ein Response Transformer erweitert WireMock um eigene Java-Logik zur dynamischen Erzeugung oder Veränderung von HTTP-Antworten. So lassen sich komplexe Mock-Szenarien realisieren, die mit statischen JSON-Dateien oder dem integrierten Response Templating allein nicht umsetzbar wären.

Generierung einer benutzerdefinierten Response 

Hier ist eine Beispielimplementierung (VariableUserCountTransformer.java) der transform-Methode, die dynamisch eine Antwort mit variabler Benutzeranzahl erzeugt:

				
					private String generateResponseWithVariableUserCount(ServeEvent serveEvent) { 
    var userCount = Integer.parseInt(serveEvent.getRequest().getHeader("User-Count")); 
    var users = Stream.generate(() -> new User( 
                    faker.name().fullName(), 
                    faker.number().numberBetween(18, 100), 
                    faker.nation().capitalCity(), 
                    compressAndBase64EncodeString(faker.lorem().characters(1_000_000)) 
            )) 
            .limit(userCount) 
            .toList(); 
    var handlebars = new Handlebars(); 
    var template = handlebars.compile("handlebars/variable-user-count-template"); 
    var context = Map.of("users", users); 
    return template.apply(context); 
} 
				
			

Request auswerten: 

Der Wert des Headers kann über das Request-Objekt des ServeEvent ausgelesen werden:

				
					var userCount = Integer.parseInt(serveEvent.getRequest().getHeader("User-Count")); 
				
			

Daten generieren: 

User ist ein vordefinierter Java-Record (User.java):

				
					public record User( 
        String name, 
        int age, 
        String location, 
        String profilePicture 
) { 
} 
				
			

Um zufällige Testdaten zu erzeugen kann zum Beispiel die Bibliothek datafaker verwendet werden 

				
					var users = Stream.generate(() -> new User( 
                faker.name().fullName(), 
                faker.number().numberBetween(18, 100), 
                faker.nation().capitalCity(), 
                compressAndBase64EncodeString(faker.lorem().characters(1_000_000)) 
            )) 
            .limit(userCount) 
            .toList(); 
				
			

Hilfsmethode: 

				
					private static String compressAndBase64EncodeString(String data) { 
    if (data == null || data.isEmpty()) { 
        return ""; 
    } 
 
    ByteArrayOutputStream bos = new ByteArrayOutputStream(); 
    try (GZIPOutputStream gzip = new GZIPOutputStream(bos)) { 
        gzip.write(data.getBytes(StandardCharsets.UTF_8)); 
        gzip.finish(); 
    } 
    byte[] compressedBytes = bos.toByteArray(); 
 
    return Base64.getEncoder().encodeToString(compressedBytes); 
} 
				
			

Template rendern: 

Ein Handlebars-Template wird kompiliert und mit einem Datenobjekt (z. B. einer Map) verbunden: 

				
					var handlebars = new Handlebars(); 
var template = handlebars.compile("handlebars/variable-user-count-template"); 
var context = Map.of("users", users); 
template.apply(context); 
				
			

Was ist Handlebars?
Handlebars ist eine Template-Engine zur dynamischen Erzeugung von Text-, HTML- oder JSON-Ausgaben. In WireMock ist dies nützlich, um Mock-Antworten anhand von Laufzeitdaten flexibel zusammenzustellen, anstatt statische JSON-Dateien zurückzugeben.

Beispiel-Template (variable-user-count-template.hbs): 

				
					[ 
{{#each users}} 
    { 
        "name": "{{name}}", 
        "age": {{age}}, 
        "location": "{{location}}", 
        "profilePicture": "{{profilePicture}}" 
    }{{#unless @last}}, 
    {{/unless}} 
{{/each}} 
] 
				
			

Funktionsweise: 

  • users: der Schlüssel eines Map-Eintrags. Der zugehörige Wert (eine Liste von User-Objekten) wird mit #each iteriert. 
  • Platzhalter{{name}}{{age}}{{location}} und {{profilePicture}} verweisen auf Felder des Java-Records User. 
  • Formatierung: Der Helper @last vermeidet ein abschließendes Komma in der JSON-Ausgabe. 

 

Antwort bauen: 

Das gerenderte Template wird schließlich in den Response-Body geschrieben: 

				
					Response.Builder 
.like(response) 
.but() 
.body(renderedTemplate) 
.build(); 
				
			

Einbindung des benutzerdefinierten Transformers 

Globale Aktivierung deaktivieren 

Standardmäßig gilt applyGlobally() = true (Transformer wird für alle Stubs angewendet). Um dies zu überschreiben (VariableUserCountTransformer.java):

				
					@Override 
public boolean applyGlobally() { 
    return false; 
}
				
			

Transformer-Namen definieren

Der Rückgabewert von getName() muss mit dem Namen in der Stub-Konfiguration übereinstimmen (VariableUserCountTransformer.java): 

				
					@Override 
public String getName() { 
    return "variable-user-count-transformer"; 
}
				
			

Transformer als Extension registrieren

Der Transformer muss beim Start explizit als Extension registriert werden: 

				
					java -jar ./target/wiremock-demo-bundled.jar 
--load-resources-from-classpath wiremock 
--extensions wiremock.transformers.VariableUserCountTransformer 
				
			

Konfiguration des Stubs 

Im folgenden Beispiel (get-variable-user-count.json) wird ein GET-Request auf /users mit dem Header User-Count konfiguriert. Der Header-Wert wird mithilfe einer Regex validiert, sodass nur ganze Zahlen ≥ 1 akzeptiert werden. In der response ist der benutzerdefinierte Transformer variable-user-count-transformer registriert: 

				
					{ 
  "request": { 
    "method": "GET", 
    "url": "/users", 
    "headers": { 
      "User-Count": { 
        "matches": "^[1-9]\\d*$" 
      } 
    } 
  }, 
  "response": { 
    "status": 200, 
    "headers": { 
      "Content-Type": "application/json" 
    }, 
    "transformers": [ 
      "variable-user-count-transformer" 
    ] 
  } 
} 
				
			

Hinweis zu Konflikten mit anderen Stubs: 

Wenn ein zweiter Stub dieselbe method und url nutzt, aber keinen User-Count-Header erfordert, kann WireMock diesen fälschlicherweise matchen. Um dies zu vermeiden, gibt es zwei Lösungsansätze: 

1. Stubs durch Header eingrenzen

Um Konflikte zu vermeiden, können Stubs durch zusätzliche Bedingungen (z. B. abwesende Header) differenziert werden (get-users.json): 

				
					{ 
  "request": { 
    "method": "GET", 
    "url": "/users", 
    "headers": { 
      "User-Count": { 
        "absent": "true" 
      } 
    } 
  }, 
  "response": { 
    "status": 200, 
    "bodyFileName": "users.json", 
    "headers": { 
      "Content-Type": "application/json" 
    } 
  } 
}
				
			

Dieser Stub wird nur für Anfragen ohne User-Count-Header geladen. 

2. Prioritäten setzen

Die Auswertungsreihenfolge von Stubs wird durch die priority-Eigenschaft festgelegt. Dabei gilt: Je kleiner der Wert, desto höher die Priorität.  

  • Höchste Priorität: 1 
  • Standardwert (falls nicht angegeben): 5 

 

Beispiel-Stub (get-variable-user-count.json) mit benutzerdefinierter Priorität: 

				
					{ 
  "priority": 1, 
  "request": { 
    "method": "GET", 
    "url": "/users", 
    "headers": { 
      "User-Count": { 
        "matches": "^[1-9]\\d*$" 
      } 
    } 
  }, 
  "response": { 
    "status": 200, 
    "headers": { 
      "Content-Type": "application/json" 
    }, 
    "transformers": [ 
      "variable-user-count-transformer" 
    ] 
  } 
} 
				
			

Spezifischere Stubs sollten in der Praxis eine höhere Priorität erhalten, da WireMock Stubs in absteigender Reihenfolge verarbeitet. 

Ergebnis der Anpassungen 

Durch die obigen Änderungen (Header-Einschränkung oder Prioritäten) wird die dynamisch generierte Antwort bevorzugt geladen, wenn der Header User-Count in der Anfrage enthalten ist. 

Beispiel-Antwort: 

				
					GET localhost:8080/users 
User-Count: 3 

[ 
  { 
    "name": "Merrill Walter", 
    "age": 38, 
    "location": "Dili", 
    "profilePicture": "H4sIAAAAAAAA/xycCXLkOg..." 
  },
  { 
    "name": "Herschel Rice", 
    "age": 37, 
    "location": "Bridgetown", 
    "profilePicture": "H4sIAAAAAAAA/xycBxLrNh..." 
  }, 
  { 
    "name": "Donnell Barrows",
    "age": 97, 
    "location": "Asuncion", 
    "profilePicture": "H4sIAAAAAAAA/ySch24kOR..." 
  } 
]
				
			

Ausführung im Docker-Container 

Um WireMock in einem Docker-Container auszuführen, ist ein Dockerfile notwendig, das Anweisungen zur Erstellung eines Docker-Images enthält. 

Was ist Docker?

Docker ist eine Plattform mit der Anwendungen und deren gesamte Umgebung (Bibliotheken, Einstellungen, Code) in sogenannte Container verpackt werden. Durch die Bereitstellung von WireMock als Docker-Container lassen sich Mock-Server reproduzierbar ausführen und einfach in Entwicklungs-, Test- und CI/CD-Umgebungen integrieren.

Ein Beispiel für ein entsprechendes Dockerfile: 

				
					# Use Amazon Corretto 25 JRE (Alpine Linux) as a parent image 
FROM amazoncorretto:25-alpine3.24 
 
# Metadaten 
LABEL version="1.0" 
LABEL description="WireMock Mockserver mit Custom Transformer" 
 
# Arbeitsverzeichnis im Container festlegen 
WORKDIR /app 
 
# Das Fat JAR in den Container kopieren 
COPY target/wiremock-demo-bundled.jar /app/wiremock-demo-bundled.jar 
 
# Exponiere den Port, auf dem WireMock standardmäßig läuft 
EXPOSE 8080 
 
# Starte WireMock, wenn der Container gestartet wird 
CMD ["java", "-jar", "wiremock-demo-bundled.jar", "--load-resources-from-classpath", "wiremock", "--extensions", "wiremock.transformers.VariableUserCountTransformer"] 
				
			

Um ein Docker-Image zu erstellen, kann man den Befehl ausführen: 

				
					docker build -t wiremock-demo-bundled:latest . 
				
			

Die Option -t legt fest, mit welchem Tag das Image versehen wird. Der Punkt . am Ende des Befehls gibt an, dass das Dockerfile im aktuellen Verzeichnis zu finden ist. 

Nachdem das Image erstellt wurde, kann man es starten:  

				
					docker run -p 8080:8080 wiremock-demo-bundled:latest 
				
			

Die Option -p/–publish dient dazu, Ports zwischen dem Host-System und dem Docker-Container zuzuordnen. Die Syntax ist immer:  

				
					-p <HOST_PORT>:<CONTAINER_PORT>
				
			

Das ist notwendig, um WireMock von externen Anwendungen auf dem Host aus ansprechen zu können. 

Wenn man jetzt auf dem Host beispielsweise eine GET-Anfrage an localhost:8080/users sendet, wird eine Antwort vom WireMock aus dem Container zurückgeliefert. 

Fazit

Die Integration von WireMock mit benutzerdefinierten Transformern ermöglicht eine flexible Lösung für das Mocking komplexer APIs und spezifischer Anwendungsfälle. In Kombination mit der Bereitstellung als Standalone-Anwendung in einem Container-Image ergeben sich wesentliche Vorteile, die das Deployment einfacher machen: 

Kernvorteile: 

  • Dynamische und Anpassbare Antworten: Benutzerdefinierte Transformer ermöglichen die Erstellung hochflexibler Mock-Antworten, die auf variablen Eingaben basieren und transformierte Daten enthalten. Dies erhöht die Realitätsnähe der Tests. 
  • Vereinfachtes Deployment und Portabilität: Die Bündelung in einer Fat JAR in Verbindung mit Docker ermöglicht ein „Build once, run anywhere“-Prinzip. Dies reduziert den Konfigurationsaufwand, vermeidet aufwendiges Mounten von Verzeichnissen und sorgt für eine konsistente Testumgebung. 

 

Unsere Erfahrung in einem Kundenprojekt bestätigt dies: Hier haben wir WireMock gezielt eingesetzt, um Services flexibel und realitätsnah zu testen. Das Ergebnis war eine signifikante Verbesserung der Systemtests auf der Plattform, wodurch wir nicht nur Standardfälle, sondern auch Randfälle zuverlässig abdecken konnten. 

Mige Lin

Mige ist Softwareentwickler bei isento

Isento Touch

Wir freuen uns,
Sie kennenzulernen.

Isento Experts

Für Kundenanfragen
Kontakt Vertrieb IT-Service Dienstleistung Softwareentwicklung Nürnberg

Miriam Barthelmes

Für interessierte Bewerber:innen
Auf dem Bild ist eine Frau zu sehen, die kurze braune Haare hat und einen orangenen Blazer trägt.

Christina Kommer