Spring RestClientTest

Alternative zu WireMock & Co?

Wenn man externe Systeme über HTTP anbindet, stellt sich schnell die Frage, wie man dafür einen Test schreiben kann.

WireMock oder MockServer sind in der Java Welt beliebte Werkzeuge, um ein Mock für externe API in der Testumgebung bereitzustellen.

Beide haben eines gemeinsam: Es wird ein echter Webserver gestartet, der die gewünschten Endpoints bereitstellt.

Nehmen wir einmal folgendes Beispiel aus einer fiktiven Spring-Anwendung:

SomeWildApiClient.java

@Service
public class SomeWildApiClient {

private RestClient restClient;

public SomeWildApiClient(
@Value("${some-wild-api.url}")
String baseUrl,
RestClient.Builder builder
) {
this.restClient = builder.baseUrl(baseUrl).build();
}

public String invokeApi() {
return restClient.get()
.uri("/some-wild-api")
.retrieve()
.body(String.class);
}
}
1 Die baseUrl muss dynamisch sein, damit wir zwischen der echten API und dem Mock umschalten können.

Ein Test mit WireMock könnte zum Beispiel so aussehen:

WireMockTest.java

@SpringBootTest
@EnableWireMock({
@ConfigureWireMock(
name = "wild-api-servicemock",
property = "some-wild-api.url"
)
})
class WireMockTest {

@InjectWireMock("wild-api-servicemock")
WireMockServer wiremock;

@Autowired
SomeWildApiClient apiClient;

@Test
void apiTest() {

var body = "This API is very wild. It's not safe to use it.";


wiremock.stubFor(get("/some-wild-api")
.willReturn(aResponse()
.withHeader("Content-Type", "text/plain")
.withBody(body)
));

var response = apiClient.invokeApi();

assertThat(response)
.isEqualTo(body);

wiremock.verify(getRequestedFor(urlEqualTo("/some-wild-api")));

}
}
1 Zuerst wird der Request definiert, auf den das Mock reagieren soll.

WireMock erzeugt einen Stub auf einem echten Webserver, den man über HTTP erreichen kann.

WireMock

Das ist alles prima, und man kann auf diese Art und Weise perfekt die Interaktion des Api-Clients mit der externen API testen.

Ein kleiner Wermutstropfen dabei ist, dass diese Art von Test recht teuer ist. WireMock startet einen echten WebServer und das ist nicht umsonst.

Bei den Spring-Test Dependencies gibt es aber auch eine technisch etwas einfachere Variante, einen ähnlichen Test zu schreiben. Geht man davon aus, dass man ja nicht den RestClient an sich testen will, sondern in erster Linie die Interaktion mit demselben, ist es unter Umständen einfacher, komplett auf die externe Kommunikation zu verzichten.

Dabei modifiziert Spring den RestClient insoweit, als er den Request gar nicht abschickt, sondern nur gegen ein internes Mock-Object schickt.

RestClientTest

Das sieht vom Code her gar nicht so anders aus, ist aber richtig flott in der Ausführung.

MockRestServiceTest.java

@RestClientTest(SomeWildApiClient.class)
@TestPropertySource(
properties = {"some-wild-api.url=http://localhost"}
)
public class MockRestServiceTest {

@Autowired
MockRestServiceServer mockServer;

@Autowired
SomeWildApiClient apiClient;

@Test
void apiTest() {

var body = "This API is very wild. It's not safe to use it.";

mockServer
.expect(requestTo("http://localhost/some-wild-api"))
.andExpect(method(HttpMethod.GET))
.andRespond(withSuccess(body, MediaType.TEXT_PLAIN));

var result = apiClient.invokeApi();

assertThat(result).isEqualTo(body);

mockServer.verify();
}
}
1 Zuletzt wird geprüft, ob die Anfrage an den MockServer gesendet wurde.

Es kommt jetzt natürlich ganz darauf an, ob man bereit ist, die Unwägbarkeiten des HTTP-Protokolls aus so einem Test auszuklammern. Diese Entscheidung müsst Ihr von Fall zu Fall selbst treffen.

Beide Ansätze verwenden in der API zur Stub-Konfiguration intensiv statisch importierte Methoden. Das ist zwar halbwegs lesbar, wenn der Code einmal fertig ist, setzt aber voraus, dass man weiß, was man da importieren muss. Gerade wenn man die ersten Schritte mit dieser Art von APIs macht, ist das manchmal schon ein wenig mühsam. Bei der WireMock.get() Methode macht IntelliJ zum Beispiel viele Vorschläge, aber die richtige Methode ist nicht dabei.

Diese Art von Test funktioniert auch mit dem RestTemplate, die Dokumentation versteckt sich in der Spring Framework Documentation.

© 2024 Thomas Kratz