Wiremock & Quarkus: How to configure it
Wiremock is a great tool that can help you in performing quality testing of your code. A fundamental step when we develop tests for applications or services that invoke others. Testing is a fundamental step in software development. And Quarkus is a Java framework specially designed for containerized, serverless and cloud application development, especially for Kubernetes. We have talked about both of them several times already.
Today we will see how we can use them together, since their use is not so simple, as it could be with another Java framework. And to achieve this we will make use of the @QuarkusTestResource annotation. In a testing class we must add this annotation associated to a class that implements the QuarkusTestResourceLifecycleManager interface. This will allow us to initialize an external service before the execution of the tests themselves. In our case, it will allow us to start the Wiremock server.
public class WiremockTestResource implements QuarkusTestResourceLifecycleManager {
public static WireMockServer wireMockServer;
@Override
public Map<String, String> start() {
wireMockServer = new WireMockServer(57001);
wireMockServer.start();
return new HashMap<String, String>();
}
@Override
public void stop() {
if (wireMockServer != null) {
wireMockServer.stop();
}
}
}
Once the class that allows to start Wiremock has been created, we will use it through the @QuarkusTestResource annotation. In this way, we will only have to indicate the stubs associated to our tests.
@QuarkusTest
@TestHTTPEndpoint(WiremockResource.class) //Resource that call external service
@QuarkusTestResource(WiremockTestResource.class)
public class WiremockResourceBasicTest {
@Test
public void getById() {
Log.info("WiremockResourceBasicTest - getById");
ObjectMapper mapper = new ObjectMapper();
Movie movie = new Movie(1L, "Denis Villeneuve", "Dune");
Log.info("WM port: " + WiremockTestResource.wireMockServer.getOptions().portNumber());
WiremockTestResource.wireMockServer.stubFor(
WireMock.get("/imdb/film/1").willReturn(WireMock.aResponse()
.withJsonBody(mapper.valueToTree(movie))
.withStatus(200).withHeader(HttpHeaders.CONTENT_TYPE, "application/json")));
when().get("/1").andReturn().then().statusCode(200)
.body("id", is(1)).body("name", is("Dune")).body("director",
is("Denis Villeneuve"));
}
}
As you can see it is very simple. Now we will see an option a little more complex but that will allow to create the Wiremock server with more configuration options and modify them dynamically. Although the development is a little bit complicated, because it has cross references.
On the one hand we will create an annotation that has the properties that we want to modify dynamically later through the annotation itself. But we will also add the annotation @QuarkusTestResource that will make reference to the class that controls the Wiremock server.
@QuarkusTestResource(value = WiremockResourceConfigurable.class, restrictToAnnotatedClass = true)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface WiremockTestAnnotation {
String port() default "57005";
}
On the other hand, we create the class that configures the Wiremock server. It will be similar to the previous example, but with three additions.
- The interface we are going to implement will be QuarkusTestResourceConfigurableLifecycleManager.
- The interface will be associated to the annotation created before. Then, through the attributes of the annotation, in the init method, we will be able to dynamically configure the server.
- And in the return of the start method, we will also be able to overwrite values of the application.properties file with the dynamic values. In this case, through this overwrite, we will be able to modify the port of the external service preconfigured for the client.
public class WiremockResourceConfigurable
implements QuarkusTestResourceConfigurableLifecycleManager<WiremockTestAnnotation> {
public static WireMockServer server;
private String port;
@Override
public void init(WiremockTestAnnotation params) {
port = params.port();
}
@Override
public Map<String, String> start() {
server = new WireMockServer(Integer.valueOf(port));
server.start();
return Map.of(
"quarkus.rest-client.\"com.home.example.service.ExternalService\".url",
"http://localhost:"+port
);
}
//...
}
Now we only have to show the test, using the annotation that would allow us to modify the port of the Wiremock server and the external service dynamically. Very similar to the previous one.
@QuarkusTest
@TestHTTPEndpoint(WiremockResource.class)
@WiremockTestAnnotation(port = "57005")
public class WiremockResourceTest {
@Inject
@ConfigProperty(name = "quarkus.rest-client.\"com.home.example.service.ExternalService\".url")
private String serverUrl;
@Test
public void getById() {
Log.info("serverUrl: " + serverUrl);
ObjectMapper mapper = new ObjectMapper();
Movie movie = new Movie(1L, "Denis Villeneuve", "Dune");
WiremockResourceConfigurable.server.stubFor(
WireMock.get("/imdb/film/1").willReturn(WireMock.aResponse().withJsonBody(mapper.valueToTree(movie))
.withStatus(200).withHeader(HttpHeaders.CONTENT_TYPE, "application/json")));
when().get("/1").andReturn().then().statusCode(200)
.body("id", is(1)).body("name", is("Dune")).body("director",is("Denis Villeneuve"));
}
}
With this we have seen one more feature to improve testing in Quarkus. And although Wiremock is a great tool that has long been very useful for test development. I would not want to omit, that it is also possible to mock the external service in a simple way with Quarkus. And this is possible by making use of the @InjectMock and @RestClient annotations. In the example shown below, ExternalService has been all the time the web client that we have mocked with Wiremock.
@QuarkusTest
@TestHTTPEndpoint(WiremockResource.class)
public class WiremockResourceInjectTest {
@InjectMock
@RestClient
ExternalService extService;
@Test
public void getById() {
Mockito.when(extService.getMovieById(Mockito.anyLong())).thenReturn(new Movie(1L, "Denis Villeneuve", "Dune"));
when().get("/1").andReturn().then().statusCode(200).body("id", is(1)).body("name", is("Dune")).body("director",
is("Denis Villeneuve"));
}
}
I hope that as always it has been useful, and if you want you can see the code here.