Spring Boot - Integration Test by Mocking Other Microservices - LinkedIn
Spring Boot - Integration Test by Mocking Other Microservices - LinkedIn
https://docs.spring.io/spring-cloud-contract/docs/2.2.7.RELEASE/reference/html/getting-started.html#getting-started-introducing-
spring-cloud-contract
If you already figured out the subject of this article only by reading the title, I
will go straight to the code that implements a producer and a consumer by
forking the spring boot own sample code.
1. Download both projects and import them into Spring Tool Suite.
2. After that, disable the dependency between projects to make it works when both
projects were opened at the same Spring Tool Suite workspace. So, you can disable
the reference between projects like that: Right-click on top the project
name>Maven>Disable Workspace Resolution. This same procedure should be done
with the Consumer and the Producer.
The main goal is to accomplish an integration test without the need to bring all
microservices up, their databases, and other components used by them to do an
integration test with your recently created/modified microservice. In addition that we
have to consider, all infrastructure should bring down and up to make sure that our tests
are deterministic.
The downside of mocking microservices is that could become out of date, even when the
tests succeed some errors could raise after deploying on production. A workaround for
this scenario could be possible if we ensure that the microservice generates the stubs
only after tests succeed. The microservice which generates the stubs is called the
producer and which microservice uses stubs in an integration test is called the consumer,
so it is very important an automatic way to deliver the stubs to ensure that they are
always up to date.
In this scenario, the stubs work as a contract between the producer and the consumer,
this contract establishes what is necessary to make a successful request to receive the
preset response. This excellent spring boot documentation explains in detail how to
implement microservices oriented by contracts. On the other hand, the implementation-
oriented by contract is a great change in the development flow. Is there another path if
you can not do this change?
One option is to take advantage of your already implemented integration tests to generate
contract definitions (only for documentation), the stubs definitions, and copy them with
the model classes to a stubs.jar package distributed automatically to other developers.
Those developers could implement consumers to interact with these stubs without the
need for the previous introduction from the developer who builds them.
Keep reading to learn more about the code shared at the beginning
The great article Integration Testing Spring Boot based Microservices using Spring
REST Docs and WireMock was my kick start to implement the integration test between
microservices using stubs with WireMock and RESTDocs. The article use gradle
instead of maven (which I use) and I felt that I needed to dive into these topics below:
Creating stubs
Request Matching
JsonPath
I will share with you some challenges and how I solved them.
Challenge:
While I am using Spring Tool Suite (IDE), the following message pops up as a problem
related to default-generateStubs, default-generateTests, and default-convert:
at org.eclipse.core.internal.jobs.Worker.run(Worker.java:63)
roleHint: org.springframework.cloud:spring-cloud-contract-maven-plugin:2.2.6.R
at org.codehaus.plexus.DefaultPlexusContainer.lookup(DefaultPlexusContainer.ja
at org.codehaus.plexus.DefaultPlexusContainer.lookup(DefaultPlexusContainer.ja
at org.apache.maven.plugin.internal.DefaultMavenPluginManager.getConfiguredMoj
... 31 more
at org.eclipse.sisu.plexus.RealmFilteredBeans$FilteredItr.next(RealmFilteredBe
at org.eclipse.sisu.plexus.RealmFilteredBeans$FilteredItr.next(RealmFilteredBe
at org.eclipse.sisu.plexus.DefaultPlexusBeans$Itr.next(DefaultPlexusBeans.java
at org.eclipse.sisu.plexus.DefaultPlexusBeans$Itr.next(DefaultPlexusBeans.java
at org.codehaus.plexus.DefaultPlexusContainer.lookup(DefaultPlexusContainer.ja
... 33 more
Solution:
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>2.2.7.RELEASE</version>
<extensions>true</extensions>
<executions>
<execution>
<id>default-convert</id>
<phase>none</phase>
</execution>
<execution>
<id>default-generateStubs</id>
<phase>none</phase>
</execution>
<execution>
<id>default-generateTests</id>
<phase>none</phase>
</execution>
</executions>
<configuration>
<testFramework>JUNIT5</testFramework>
</configuration>
</plugin>
Challenge:
Solution:
Using the @ContextConfiguration from Spring Boot, the WireMockInitializer class was
created to define a random port to be used by the WireMock server to make the beer-
api-producer-restdocs stubs available. Along with the possibility to specify which
properties from application.yml should be overwritten with the new url used by
WireMock.
WireMockInitializer.java
package com.example;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.cloud.contract.wiremock.WireMockSpring;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.SocketUtils;
import com.github.tomakehurst.wiremock.common.Slf4jNotifier;
import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTe
@Override
applicationContext.getBeanFactory().registerSingleton("Options
setupServicesUrl(randomPort, applicationContext);
factory.notifier(new Slf4jNotifier(true));
factory.extensions(new ResponseTemplateTransformer(false));
return factory;
application.yml
spring:
application.name: beer-api-consumer
beer-api-producer-restdocs:
url: http://localhost:8081
logging:
level:
org.springframework.cloud: debug
BeerControllerTest.java
package com.example;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.AutoConfigureJsonTeste
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMo
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
/**
*/
@AutoConfigureJsonTesters
@ContextConfiguration(initializers = {WireMockInitializer.class})
//remove::start[]
@AutoConfigureWireMock(stubs = "classpath:/META-INF/com.example/beer-api-produ
//remove::end[]
this.mockMvc.perform(MockMvcRequestBuilders.post("/beer")
.contentType(MediaType.APPLICATION_JSON)
.content(this.json.write(new Person("marcin",
.andExpect(status().isOk())
//remove::end[]
this.mockMvc.perform(MockMvcRequestBuilders.post("/beer")
.contentType(MediaType.APPLICATION_JSON)
.content(this.json.write(new Person("marcin",
.andExpect(status().isOk())
.andExpect(content().string("GET LOST"));
//remove::end[]
BeerController.java
package com.example;
import java.net.MalformedURLException;
import java.net.URI;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
*/
@RestController
class BeerController {
@Value("${beer-api-producer-restdocs.url}")
BeerController(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
@RequestMapping(method = RequestMethod.POST,
value = "/beer",
consumes = MediaType.APPLICATION_JSON_VALUE)
.post(URI.create(urlBeerApiPro
.contentType(MediaType.APPLICA
.body(person),
Response.class);
switch (response.getBody().status) {
case OK:
default:
//remove::end[return]
class Person {
this.name = name;
this.age = age;
public Person() {
class Response {
enum ResponseStatus {
OK, NOT_OK
All this approach presented depends on a team where each member implements the
integration tests as each one builds/modifies a microservice.
Denunciar esto
Publicado por
Renan Franca 2 artículos Seguir
Software Engineer | Microservices | Enjoy ☕🍃☸️🤓🍼
Fecha de publicación: 1 año
Reacciones
Ka
mbiz
H.
Renan Franca
Software Engineer | Microservices | Enjoy ☕🍃☸️🤓🍼
Seguir