Better Page Objects strategy using AjaxElementLocatorFactory with Selenium and Java
August 22, 2022Soft Asserts – Why should you use them for Unit and Integration tests?
March 27, 2023Introduction
Mocks are present in any modern development process to speed up software delivery.
Your API service depends on another API, internally or a 3rd party one. Mocks are 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 feel something is still missing.
When we need to look 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 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.
The problem we are trying to solve
Scenario
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, as well as 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?
Technical scenario
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.
How to solve this problem
The bad practice here would be all the services, that need to use the Restrictions API 3rd party library, create their 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.
Service Virtualization
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 of 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:
- provide an endpoint for not completed APIs
- provide an API for the 3rd parties dependencies you can’t control
- when needed to be consumed from different perspectives (backend, frontend, test)
How Wiremock can solve it?
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 that will reply based on certain conditions.
We will create an approach that is “Dockerized” so we can use it in any orchestration system, using the Stubbing & Verifying feature.
Wiremock internals
When we don’t use the Wiremock Java library, we can use it via the JSON API, creating request and response JSON files that match the expected requests 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.
Wiremock will understand the matched request and reply accordingly based on the defined mappings.
How to create the request and response?
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 and playback to make things shorter and clearer.
Technical Solution
Code explanation
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());
}
Solving this problem with Wiremock
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 are two main mappings we must do:
- Simulation, returning HTTP 403
- Restriction, returning HTTP 200 and the appropriate message
The mappings for Wiremock will look like the following.
Mapping for the Simulation API
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"
}
Mapping for Restrictions
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.
Building a Docker image
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. The good news is that Wiremock has a way to run the standalone approach using Docker 👉🏻 https://wiremock.org/docs/standalone/docker
We can use the Docker Compose example provided to build the Docker image:
version: '3.9'
services:
wiremock:
image: "wiremock/wiremock:latest"
container_name: wiremock-credit-restriction-api
ports:
– "8087:8080"
volumes:
# copy the local Wiremock files (__files and mappings folder) into the container
– ./__files:/home/wiremock/__files
– ./mappings:/home/wiremock/mappings
entrypoint: ["/docker-entrypoint.sh", "–global-response-templating", "–disable-gzip", "–verbose"]
In the docker-compose.yml
we are using the latest Wiremock available Docker image as a service, mapping the internal Wiremock 8080 port to the 8087 external port (lines 3 to 9).
We also need to map the request and response files we created, available in the __files
and mappings
folder. This is done by mapping the volumes
from the local file to the internal Wiremock folder in the container (lines 9 to 12).
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.
Now we will put all these examples into practice.
Simulating the necessity of the Wiremock Service Virtualization
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.
- Clone the credit-simulator-api project
- Run the following command in your Terminal to start the application:
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.
How to add the Wiremock Service Virtualization solution?
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:
- Clone the wiremock-credit-restriction-api
- Start your Docker application
- Run the following command in your Terminal, to build the Docker image and start it
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.
Important mention about the example projects
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.