Suppose you are developing a Spring Boot client application that needs to communicate with a remote server API. You are for sure going to write a communication layer (let's call it Repository
) between your application and the server, and you are going to test it in some way. So you need to simulate the server behavior i.e. responses and you have some ways to do this in a unit test:
But if you want to load the Spring context and build a sort of integration test, you have these alternatives:
@MockBean
annotation to mock the Repository response, but also in this case you are not calling the RestTemplate and thus overtaking some of the application logicMockRestServiceServer
classI decided to exclude the Mockito approach because I wanted to build an integration test and I had time constraints so I could not spend much time in mocking the RestTemplate responses. So I went for the MockServer
solution, also because I wanted to be able to launch the client application on my local machine and be able to open webpages and see some real mock data inside. So, basically I used the same approach both for my test
and local
profiles. The problem with this is that I needed to have two different configurations for the same bean representing the remote server, and they both had to stay in the src/main/java
directory to be able to also run the application on the local machine, for example:
@Component("remoteServer")
@Profile("!test")
public class RealRemoteServer {
@Value("${remote.server.base.url}")
private String remoteBaseURL;
@PostConstruct
public void init() {
log.info("Remote server at " + remoteBaseURL)
// nothing else to do here
}
}
@Component("remoteServer")
@Profile("test")
public class MockRemoteServer {
@Value("${remote.server.base.url}")
private String remoteBaseURL;
@PostConstruct
public void init() {
log.info("Remote server at " + remoteBaseURL)
// start MockServer and define mock responses
}
@PreDestroy
public void destroy() {
// shut down MockServer
}
}
Moreover, to be able to run the MockServer also on the local machine, I needed to have it as a non test scoped dependency in my pom file. This had to be changed, and this post is about the solution I found.
I created this project as an example, so we have two profiles test and production, a repository supposed to communicate with a remote server API and the test class.
So, let's say that we have a ProductRepository
that defines a findAll
and a findOne
methods, like:
package me.fabiomaffioletti.msc.repository;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Repository;
import org.springframework.web.client.RestTemplate;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j;
import me.fabiomaffioletti.msc.util.HttpEntityBuilder;
import me.fabiomaffioletti.msc.dto.ProductDTO;
@Log4j
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Repository
public class ProductRepositoryImpl implements ProductRepository {
private final RestTemplate restTemplate;
@Value("${remote.server.uri.products}")
private String productsURI;
@Override
public ResponseEntity<List<ProductDTO>> findAll() {
HttpEntityBuilder<Void> httpEntityBuilder = new HttpEntityBuilder<>();
return restTemplate.exchange(productsURI, HttpMethod.GET, httpEntityBuilder.build(), new ParameterizedTypeReference<List<ProductDTO>>() {});
}
@Override
public ResponseEntity<ProductDTO> findOne(Long id) {
HttpEntityBuilder<Void> httpEntityBuilder = new HttpEntityBuilder<>();
return restTemplate.exchange(productsURI + "/{id}", HttpMethod.GET, httpEntityBuilder.build(), ProductDTO.class, id);
}
}
And the related test class made like this:
package me.fabiomaffioletti.msc.repository;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import me.fabiomaffioletti.msc.MockserverCondensedTestConfiguration;
import me.fabiomaffioletti.msc.dto.ProductDTO;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
@RunWith(SpringRunner.class)
@MockserverCondensedTestConfiguration
public class ProductRepositoryTest {
@Autowired
private ProductRepository productRepository;
@Test
public void testItShouldRetrieveTheListOfProducts() {
ResponseEntity<List<ProductDTO>> responseEntity = productRepository.findAll();
assertThat(responseEntity.getStatusCode(), is(HttpStatus.OK));
assertThat(responseEntity.getBody(), hasSize(3));
}
@Test
public void testItShouldRetrieveAProductWithTheGivenId() {
ResponseEntity<ProductDTO> responseEntity = productRepository.findOne(1L);
assertThat(responseEntity.getStatusCode(), is(HttpStatus.OK));
assertThat(responseEntity.getBody().getId(), is(1L));
assertThat(responseEntity.getBody().getName(), is(equalTo("product one")));
}
}
Having @MockserverCondensedTestConfiguration
specifying the test intent:
package me.fabiomaffioletti.msc;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest
@ActiveProfiles("test")
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MockserverCondensedTestConfiguration {
}
While in my application.properties
file I have defined the remote server endpoints, with a the base url as a variable defined in each profiled properties file:
remote.server.uri.products=${remote.server.base.url}/products
# in application-test.properties
remote.server.base.url=http://localhost:9000
# in application-prod.properties
remote.server.base.url=http://remote-server-real-url
So now, if you try to run the tests, you will get this error:
org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:9000/products": Connection refused; nested exception is java.net.ConnectException: Connection refused
That's because there is nothing, real nor mock, listening to that endpoint, and responding with something. To mock the response it is possible to use the MockRestServiceServer
class, contained in the Spring test dependencies. Let's see an example and how I decided to change the approach of using it in a particular case: the ProductRepositoryTest
class becomes like this one:
package me.fabiomaffioletti.msc.repository;
import java.io.IOException;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.test.web.client.response.DefaultResponseCreator;
import org.springframework.web.client.RestTemplate;
import me.fabiomaffioletti.msc.MockserverCondensedTestConfiguration;
import me.fabiomaffioletti.msc.dto.ProductDTO;
import static java.nio.file.Files.readAllBytes;
import static java.nio.file.Paths.get;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.springframework.test.web.client.ExpectedCount.times;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
@RunWith(SpringRunner.class)
@MockserverCondensedTestConfiguration
public class ProductRepositoryTest {
@Autowired
private ProductRepository productRepository;
@Autowired
private RestTemplate restTemplate;
@Value("${remote.server.uri.products}")
private String productsURI;
private MockRestServiceServer mockRestServiceServer;
@Before
public void setUp() {
mockRestServiceServer = MockRestServiceServer.bindTo(restTemplate).build();
}
@Test
public void testItShouldRetrieveTheListOfProducts() throws IOException {
byte[] responseBody = readAllBytes(get("src", "test", "resources", "mock", "response", "remote.server.response.products.findAll.200.json"));
DefaultResponseCreator responseCreator = withStatus(HttpStatus.OK).body(responseBody).contentType(MediaType.APPLICATION_JSON_UTF8);
mockRestServiceServer.expect(times(1), requestTo(productsURI)).andExpect(method(HttpMethod.GET)).andRespond(responseCreator);
ResponseEntity<List<ProductDTO>> responseEntity = productRepository.findAll();
assertThat(responseEntity.getStatusCode(), is(HttpStatus.OK));
assertThat(responseEntity.getBody(), hasSize(3));
mockRestServiceServer.verify();
}
@Test
public void testItShouldRetrieveAProductWithTheGivenId() throws IOException {
byte[] responseBody = readAllBytes(get("src", "test", "resources", "mock", "response", "remote.server.response.products.findOne.1.200.json"));
DefaultResponseCreator responseCreator = withStatus(HttpStatus.OK).body(responseBody).contentType(MediaType.APPLICATION_JSON_UTF8);
mockRestServiceServer.expect(times(1), requestTo(productsURI + "/1")).andExpect(method(HttpMethod.GET)).andRespond(responseCreator);
responseBody = readAllBytes(get("src", "test", "resources", "mock", "response", "remote.server.response.products.findOne.2.200.json"));
responseCreator = withStatus(HttpStatus.OK).body(responseBody).contentType(MediaType.APPLICATION_JSON_UTF8);
mockRestServiceServer.expect(times(1), requestTo(productsURI + "/2")).andExpect(method(HttpMethod.GET)).andRespond(responseCreator);
ResponseEntity<ProductDTO> responseEntity = productRepository.findOne(1L);
assertThat(responseEntity.getStatusCode(), is(HttpStatus.OK));
assertThat(responseEntity.getBody().getId(), is(1L));
assertThat(responseEntity.getBody().getName(), is(equalTo("product one")));
responseEntity = productRepository.findOne(2L);
assertThat(responseEntity.getStatusCode(), is(HttpStatus.OK));
assertThat(responseEntity.getBody().getId(), is(2L));
assertThat(responseEntity.getBody().getName(), is(equalTo("product two")));
mockRestServiceServer.verify();
}
}
So for each test, you need to define the mock server expectations, and optionally you can define how many times the remote endpoint gets called in the single test. Tests are now green. This is already a big step ahead compared to the previous approach. But this is an example project, it contains just one repository and one test class with two tests. In my use case I needed to answer these two questions:
It became clear that I could not change each test and set expectations in each test. So I came up with a different approach.
Basically I wanted to define the expectations in just one place, like it happened with the MockServer. So I created this class, in the src/test/java
directory, that is only active when using the test profile. Moreover I added this class to the one picked up by Spring when loading the context, so the @Bean
is configured. Here are the resulting classes:
package me.fabiomaffioletti.msc;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.test.web.client.response.DefaultResponseCreator;
import org.springframework.web.client.RestTemplate;
import lombok.extern.log4j.Log4j;
import static java.nio.file.Files.readAllBytes;
import static java.nio.file.Paths.get;
import static org.springframework.test.web.client.ExpectedCount.times;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
@Log4j
@Profile("test")
public class MockRemoteServerConfiguration {
@Autowired
private RestTemplate restTemplate;
@Value("${remote.server.uri.products}")
private String productsURI;
@Bean
public MockRestServiceServer mockRestServiceServer() throws Exception {
MockRestServiceServer mockRestServiceServer = MockRestServiceServer.bindTo(restTemplate).build();
byte[] responseBody = readAllBytes(get("src", "test", "resources", "mock", "response", "remote.server.response.products.findAll.200.json"));
DefaultResponseCreator responseCreator = withStatus(HttpStatus.OK).body(responseBody).contentType(MediaType.APPLICATION_JSON_UTF8);
mockRestServiceServer.expect(times(1), requestTo(productsURI)).andExpect(method(HttpMethod.GET)).andRespond(responseCreator);
responseBody = readAllBytes(get("src", "test", "resources", "mock", "response", "remote.server.response.products.findOne.1.200.json"));
responseCreator = withStatus(HttpStatus.OK).body(responseBody).contentType(MediaType.APPLICATION_JSON_UTF8);
mockRestServiceServer.expect(times(1), requestTo(productsURI + "/1")).andExpect(method(HttpMethod.GET)).andRespond(responseCreator);
responseBody = readAllBytes(get("src", "test", "resources", "mock", "response", "remote.server.response.products.findOne.2.200.json"));
responseCreator = withStatus(HttpStatus.OK).body(responseBody).contentType(MediaType.APPLICATION_JSON_UTF8);
mockRestServiceServer.expect(times(1), requestTo(productsURI + "/2")).andExpect(method(HttpMethod.GET)).andRespond(responseCreator);
return mockRestServiceServer;
}
}
package me.fabiomaffioletti.msc;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest(classes = {MockserverCondensedApplication.class, MockRemoteServerConfiguration.class})
@ActiveProfiles("test")
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MockserverCondensedTestConfiguration {
}
Of course I also removed the MockRestServiceServer stuff from the test class, so it is exactly as the beginning. This means that there is no need to change the tests already written previously, which, as I wrote before, could be hundreds or thousands. The resulting test class becomes:
package me.fabiomaffioletti.msc.repository;
import java.io.IOException;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import me.fabiomaffioletti.msc.MockserverCondensedTestConfiguration;
import me.fabiomaffioletti.msc.dto.ProductDTO;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
@RunWith(SpringRunner.class)
@MockserverCondensedTestConfiguration
public class ProductRepositoryTest {
@Autowired
private ProductRepository productRepository;
@Test
public void testItShouldRetrieveTheListOfProducts() throws IOException {
ResponseEntity<List<ProductDTO>> responseEntity = productRepository.findAll();
assertThat(responseEntity.getStatusCode(), is(HttpStatus.OK));
assertThat(responseEntity.getBody(), hasSize(3));
}
@Test
public void testItShouldRetrieveAProductWithTheGivenId() throws IOException {
ResponseEntity<ProductDTO> responseEntity = productRepository.findOne(1L);
assertThat(responseEntity.getStatusCode(), is(HttpStatus.OK));
assertThat(responseEntity.getBody().getId(), is(1L));
assertThat(responseEntity.getBody().getName(), is(equalTo("product one")));
responseEntity = productRepository.findOne(2L);
assertThat(responseEntity.getStatusCode(), is(HttpStatus.OK));
assertThat(responseEntity.getBody().getId(), is(2L));
assertThat(responseEntity.getBody().getName(), is(equalTo("product two")));
}
}
Now, if you run the tests, you will... get an error, like this:
java.lang.AssertionError: Request URI
Expected :http://localhost:9000/products
Actual :http://localhost:9000/products/1
This happend because the MockRestServiceServer expectations have an order, that is following the creation and addition order specified in the MockRemoteServerConfiguration
definition. You can pass an argument to the MockRestServiceServer
build method, to specify which "expectation manager" you want to use. Spring gives you two: SimpleRequestExpectationManager
which is the deafult one, and UnorderedRequestExpectationManager
. Both count the number of expected calls to the mock server expectation and try to match it with the actual number of calls happened during the test. The difference between the two is that UnorderedRequestExpectationManager
ignores the declaration order.
So, if you know how many calls are made to the mock server in the whole test suite, you can try to use UnorderedRequestExpectationManager
and specify, for each expectation, the number of expected calls, in the times()
method. Unfortunately this is not my case because I don't know how many calls are made to each endpoint and in which order they are made, so I made my own implementation of the AbstractRequestExpectationManager
which is actually an extension of the UnorderedRequestExpectationManager
.
package me.fabiomaffioletti.msc;
import org.springframework.test.web.client.UnorderedRequestExpectationManager;
public class NoResetRequestExpectationManager extends UnorderedRequestExpectationManager {
@Override
public void reset() {
// do not reset or clear the expectation list
}
}
As you see, if you compare it with the UnorderedRequestExpectationManager
reset method, this new one does not do anything, so basically it it saying that we don't care about the order of the calls and we don't care about the number of calls executed. The last things to do are:
NoResetRequestExpectationManager
times(x)
to min(1)
for each expectation, so we grant that the minimum number of calls to each endpoint is 1Here we go with the final class:
package me.fabiomaffioletti.msc;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.test.web.client.response.DefaultResponseCreator;
import org.springframework.web.client.RestTemplate;
import lombok.extern.log4j.Log4j;
import static java.nio.file.Files.readAllBytes;
import static java.nio.file.Paths.get;
import static org.springframework.test.web.client.ExpectedCount.min;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
@Log4j
@Profile("test")
public class MockRemoteServerConfiguration {
@Autowired
private RestTemplate restTemplate;
@Value("${remote.server.uri.products}")
private String productsURI;
@Bean
public MockRestServiceServer mockRestServiceServer() throws Exception {
MockRestServiceServer mockRestServiceServer = MockRestServiceServer.bindTo(restTemplate).build(new NoResetRequestExpectationManager());
byte[] responseBody = readAllBytes(get("src", "test", "resources", "mock", "response", "remote.server.response.products.findAll.200.json"));
DefaultResponseCreator responseCreator = withStatus(HttpStatus.OK).body(responseBody).contentType(MediaType.APPLICATION_JSON_UTF8);
mockRestServiceServer.expect(min(1), requestTo(productsURI)).andExpect(method(HttpMethod.GET)).andRespond(responseCreator);
responseBody = readAllBytes(get("src", "test", "resources", "mock", "response", "remote.server.response.products.findOne.1.200.json"));
responseCreator = withStatus(HttpStatus.OK).body(responseBody).contentType(MediaType.APPLICATION_JSON_UTF8);
mockRestServiceServer.expect(min(1), requestTo(productsURI + "/1")).andExpect(method(HttpMethod.GET)).andRespond(responseCreator);
responseBody = readAllBytes(get("src", "test", "resources", "mock", "response", "remote.server.response.products.findOne.2.200.json"));
responseCreator = withStatus(HttpStatus.OK).body(responseBody).contentType(MediaType.APPLICATION_JSON_UTF8);
mockRestServiceServer.expect(min(1), requestTo(productsURI + "/2")).andExpect(method(HttpMethod.GET)).andRespond(responseCreator);
return mockRestServiceServer;
}
}
Tests are now green. Here is the structure of the project at this point:
What we got:
src/main/java
directory everything related to testsBut we still have a problem, i.e. running the application on the local machine, with mock data, so avoiding the use of a real instance of the remote server. For this I will use WireMock
in a docker container.
First thing to do is to pull the WireMock docker image: docker pull rodolpheche/wiremock
. Then, of course, we need to create a local profile configuration (application-local.properties), that could be something like:
remote.server.base.url=http://localhost:1888
This means that we intend to run the WireMock on localhost and port 1888. That's why, from the project root, we need to run the docker image with this command:
docker run -d -v $PWD/stub:/home/wiremock --name mockserver -p 1888:8080 -p 1881:8081 rodolpheche/wiremock
Now if you go to http://localhost:1888/__admin/
you should see an empty mappings json. And in your project root directory there will be a stub directory containing mappings
and __files
: the first one will hold the mappings definitions, the second one the response bodies. For example, the mapping file for the findAll products endpoint could be something like this:
{
"request": {
"method": "GET",
"urlPath": "/products"
},
"response": {
"status": 200,
"bodyFileName": "remote.server.response.products.findAll.200.json",
"headers": {
"Content-Type": "application/json"
}
}
}
And the response, in the __files
directory, something like:
[
{
"id": 1,
"name": "product one"
},
{
"id": 2,
"name": "product two"
},
{
"id": 3,
"name": "product three"
},
{
"id": 4,
"name": "product four"
},
{
"id": 5,
"name": "product five"
}
]
Now, if the docker is restarted with docker restart mockserver
and you navigate to http://localhost:1888/__admin/
, you should see all the mappings defined. How to test that everything will be ok when the application will be running with the local profile? We need a controller to do that (let me skip the service layer and autowire the repository directly):
package me.fabiomaffioletti.msc.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j;
import me.fabiomaffioletti.msc.dto.ProductDTO;
import me.fabiomaffioletti.msc.repository.ProductRepository;
@Log4j
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@RestController
public class ProductController {
private final ProductRepository productRepository;
@GetMapping("/products")
public ResponseEntity<List<ProductDTO>> findAll() {
return productRepository.findAll();
}
@GetMapping("/products/{id}")
public ResponseEntity<ProductDTO> findOne(@PathVariable Long id) {
return productRepository.findOne(id);
}
}
If you start the application and navigate to http://localhost:8080/products
you should see the response coming from the WireMock server on Docker. Here is the resulting structure:
You can find the example project here
comments powered by Disqus