Mocks are present in any modern development process to speed up software delivery.
Your API service has a dependency on another API, internally or a 3rd party one. There are mocks making sure everything is working as expected at any new change, no matter if it’s in the dependency or not. You work in a good environment, and you feel that something is still missing.
When we need to look out outside the service bubble, which means unit and integration tests, there are some challenges in testing the application. You will learn how to extend the testing coverage and have confidence in delivering the best software by introducing a new testing approach.
Note!
We had a similar article about it the last year, but focusing on the unit test: https://www.javaadvent.com/2021/12/improving-quality-by-mocking-apis-with-wiremock.html
So, you can get spectacular examples of how to apply it in the unit layer.
There’s an API called credit-simulator-api (Simulations API) which will perform normal CRUD operations to get a loan. This API depends on another one which will first check for restrictions, so we can only continue if there is no restriction. Let’s name it credit-restriction-api (Restrictions API) and consider it a 3rd party API.
As it’s a 3rd party library, you still need to use it for testing. Remember that you have unit tests mocked for this, but what about integration tests and how other teams, who also use this library, will use it?
You need to test this integration within this 3rd party, all well other internal services. You orchestrate different services to test your application in a Kubernetes platform, where everything is delivered as a Docker image.
How can we test the credit-simulator-api as we still don’t have access to the 3rd party library, where more teams probably have the same problem?
How can we test the consumption of the services we created by either a Web App or Mobile App?
The credit-simulator-api will save a loan simulation through a POST request, which will check before saving it if there’s any restriction using the credit-restriction-api (the 3rd party API). If there’s a restriction the API won’t act meaning no loan simulation will be saved.
The key attribute to check for a restriction is called cpf.
In the credit-restriction-api case, the response without a restriction is an HTTP 404 (not found any restriction), whereas the one with restriction is an HTTP 200 with a message in the response. This will be translated by an HTTP 403 in the credit-simulator-api with a message in the response.
The bad practice here would be all the services, that need to use the Restrictions API 3rd party library, create their own solution, and increase the general maintenance.
One of the solutions is to create a “global mock” that can be used not only by the credit-simulator-api in isolation but by any other service that needs it, as well as the usage in persistent contexts. That’s why we will talk about the service virtualization approach.
This approach will emulate/simulate the behavior of specific applications, focusing more on the API-driven ones, most of the time cloud-based. It solves the dependency gap, during the development process where we can exercise the whole application exercise this behavior of a real component which is required to test and deliver the product.
In short, it created a virtual service to simulate real behavior.
It has innumerable benefits using it, such as:
The focus here is not to explain how Wiremock works, because we have the official documentation for that, and the previous years’ post about it.
The way Wiremock will solve it is in the approach! Instead of using it in the service, as a support tool to write the mocks, we will create a “persistent mock” server which will reply based on certain conditions.
We will create an approach that will be “Dockerized”, so we can use it in any orchestration system, using the Stubbing & Verifying feature.
When we don’t use the Wiremock Java library, we can use it via the JSON API, creating request and response JSON files that will match the expected request and responses.
You can find it in the official documentation, but it’s good to make it clear: we can have a folder called mapping
, which will contain the requests that will be matched and the __files
which will contain the response.
Basically, Wiremock will understand the matched request and reply accordingly base on the mappings we have defined.
The normal method would be either creating it manually if you have some previous experience with Wiremock or using the Record & Playback feature to proxy all the requests and responses and let the tool create it for you. Instead of explaining it we will see the end files, and understand how it will reply to our application.
In this article, we will create it without the Record & Playback to make things shorter and clear.
We will use the credit-simulator-api project to illustrate the problem, also creating the solution using Wiremock.
The main focus is on the controller, where the POST
method first checks if there is a restriction, consuming the Restrictions API using the checkForRestriction()
method.
@PostMapping("/") | |
@ResponseStatus(HttpStatus.CREATED) | |
public ResponseEntity<Simulation> newSimulation(@Valid @RequestBody SimulationDto simulation) { | |
checkForRestriction(simulation.getCpf()); | |
Simulation createdSimulation = repository.save(new ModelMapper().map(simulation, Simulation.class)); | |
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{cpf}"). | |
buildAndExpand(createdSimulation.getCpf()).toUri(); | |
return ResponseEntity.created(location).build(); | |
} |
The private method checkForRestriction()
hit the Restrictions API endpoint. When the response is null
, which means that the return is HTTP 404 meaning no restriction, nothing will happen. When the response is HTTP 200 meaning a restriction, a custom exception will be thrown.
private void checkForRestriction(String cpf) throws SimulationException { | |
RestTemplate template = new RestTemplateBuilder().errorHandler(new RestTemplateErrorHandler()).build(); | |
String restrictionsEndpoint = String.format("%s:%s%s", env.getProperty("restrictions.host"), | |
env.getProperty("restrictions.port"), env.getProperty("restrictions.path")); | |
var response = template.getForObject(restrictionsEndpoint, MessageDto.class, cpf); | |
if (response != null) throw new ResponseStatusException(HttpStatus.FORBIDDEN, response.message()); | |
} |
The main goal is to solve the issue above when the mock is available only in the unit layer. We need to create two mappings using Wiremock: one for the restriction and another for the simulation.
We will exercise only the negative scenario, where the simulation will return HTTP 403 because the CPF we are using has a restriction, remember about the image at the beginning of this article.
When this is the case the Restrictions API returns HTTP 200 confirming that a restriction was found.
There’re two main mappings we must do:
The mappings for Wiremock will look like the following.
This mapping will match the POST
method for the URL /api/v1/simulations/
when the cpf
attribute in the response body is 9709326014
, returning the HTTP 403 and the response defined in the file simulation_with_restriction_response.json
.
{ | |
"name" : "Simulation request containing a restriction", | |
"request" : { | |
"url" : "/api/v1/simulations/", | |
"method" : "POST", | |
"bodyPatterns" : [ | |
{ | |
"equalToJson" : { | |
"cpf": "97093236014" | |
} | |
} ] | |
}, | |
"response" : { | |
"status" : 403, | |
"bodyFileName" : "simulation_with_restriction_response.json", | |
"headers" : { | |
"Content-Type" : "application/json" | |
} | |
} | |
} |
This is the response matched in the "bodyFileName" : "simulation_with_restriction_response.json"
:
{ | |
"message": "CPF has a restriciton" | |
} |
This mapping will match the GET
method for the URL /api/v1/restrictions/97093236014
, returning the HTTP 200 and the response defined in the file restrictions_response_200.json
.
{ | |
"name" : "Restriction request for a CPF with restriction", | |
"request" : { | |
"url" : "/api/v1/restrictions/97093236014", | |
"method" : "GET" | |
}, | |
"response" : { | |
"status" : 200, | |
"bodyFileName" : "restrictions_response_200.json", | |
"headers" : { | |
"Content-Type" : "application/json" | |
} | |
} | |
} |
This is the response matched in the “bodyFileName" : "restrictions_response_200.json"
:
{ | |
"message": "CPF 97093236014 has a restriction" | |
} |
Now we have this “global mock” which means the service virtualization approach, ready for use.
A better solution, instead of running it standalone using the Wiremock jar file, would be the creation of a Docker image containing the files and the standalone process. We will divide it into a Dockerfile
to generate the image and a docker-compose
file to run it locally, for educational purposes. Remember that you can generate and push. custom docker image as a real solution.
The Dockerfile
is based on an alpine Java 8 version to reduce the image size. It downloads the Wiremock java standalone jar file, enabling the internal port as 8088, starting it in a verbose mode in the ENTRYPOINT
.
FROM anapsix/alpine-java:8 | |
RUN apk add –update curl && \ | |
rm -rf /var/cache/apk/* | |
ENV WM_PACKAGE wiremock-jre8-standalone | |
ARG WM_VERSION=2.35.0 | |
RUN mkdir /$WM_PACKAGE | |
WORKDIR /$WM_PACKAGE | |
RUN curl -sSL -o $WM_PACKAGE.jar https://repo1.maven.org/maven2/com/github/tomakehurst/$WM_PACKAGE/$WM_VERSION/$WM_PACKAGE-$WM_VERSION.jar | |
EXPOSE 8088 | |
ENTRYPOINT ["java","-jar","wiremock-jre8-standalone.jar","–verbose", "–port", "80"] |
In the docker-compose.yml
we will build the docker image based on the Dockerfile
we have defined above, plus copy the response files from the __files
folder, as well as the mapping files from the mappings
folder into the Wiremock inside the image.
This will make available the mappings globally when the Docker image is running. You can run it during your service build and, later, create a persistent image to use in the Kubernetes platform.
version: '3.9' | |
services: | |
wiremock: | |
build: | |
context: ./ | |
dockerfile: Dockerfile | |
image: wiremock-custom | |
container_name: wiremock-credit-restriction-api | |
ports: | |
– "8088:80" | |
volumes: | |
# we copy our scripts onto the container | |
– ./__files:/wiremock-jre8-standalone/__files | |
– ./mappings:/wiremock-jre8-standalone/mappings |
Now we will put all these examples into practice.
The Simulations API exists, and we will use it to get to the error message where we need the mock to use the API properly.
mvn spring-boot:run
Now, try to create a new Simulation using the POST request. You can do it using the following command:
curl -X 'POST' \ | |
'http://localhost:8089/api/v1/simulations/' \ | |
-H 'accept: */*' \ | |
-H 'Content-Type: application/json' \ | |
-d '{ | |
"cpf": "97093236014", | |
"email": "john.doe@gmail.com", | |
"name": "John Doe", | |
"installments": 3, | |
"insurance": true, | |
"amount": 1200 | |
}' |
Alternatively, you can access the OpenAPI spec using the following URL when the application is running, and “Try it out” the POST request: http://localhost:8089/swagger-ui/index.html
The result of this request is an HTTP 500, containing plain text which contains the following:
The following error was encountered while trying to retrieve the URL:
http://localhost:8089/api/v1/simulations/
This is happening because the controller for the POST request has a call to the Restrictions API, which is unavailable.
There’s already a solution published for that, the same one explained in the “Solving this problem with Wiremock” section. To exercise it you need to:
docker-compose up --build
You will see the Wiremock banner.
Now, make the same request to the Simulations API:
curl -X 'POST' \ | |
'http://localhost:8089/api/v1/simulations/' \ | |
-H 'accept: */*' \ | |
-H 'Content-Type: application/json' \ | |
-d '{ | |
"cpf": "97093236014", | |
"email": "john.doe@gmail.com", | |
"name": "John Doe", | |
"installments": 3, | |
"insurance": true, | |
"amount": 1200 | |
}' |
You will now receive the HTTP 403 with the following response body:
{
"message": "CPF has a restriction"
}
Congratulations! Now you have the service virtualization approach running inside a Docker container that can be shared across the teams.
In the credit-simulator-api you can see that the application.properties has the host
, port
, and path
configuration for the Restrictions API. The most important correlation here is that the Wiremock in the Docker image is also responding at the same host
and port
defined in the file.
The wiremock-credit-restriction-api contains all the necessary files to start the Docker image using the mappings created. Remember that the mappings and responses, ideally, must be placed in this context to avoid different paths for these files.