Testing Spring Boots caching behavior in a RestClientTest - java

I have the following scenario:
A Service calls some rest endpoint to query a resource.
The result of this query should be cached for one day and then requeried.
To verify this behavior I wrote a test that calls the service method 2 times and expects that the rest service was actually only queried one time.
Here would be the class that calls the endpoint:
#EnableCaching
#Service
public class CachedResourceClient {
private final RestTemplate restTemplate;
public CachedResourceClient(#Value("${api.host}") String apiHost, RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder
.rootUri(apiHost)
.build();
}
#Cacheable("resource")
public Optional<Resource> fetchResource() {
try {
return Optional.of(restTemplate.getForObject("/resource", Resource.class));
} catch (HttpClientErrorException | HttpServerErrorException | UnknownHttpStatusCodeException e) {
// logging
return Optional.empty();
}
}
}
and then we have this test here:
#RestClientTest(CachedResourceClient.class)
public class CachedResourceClientTests {
#Autowired
private MockRestServiceServer server;
#Autowired
private CachedResourceClient cachedResourceClient;
#Test
public void fetchResource() throws JsonProcessingException {
server.expect(requestTo("/resource"))
.andRespond(withSuccess("{ resource: 'some' }", MediaType.APPLICATION_JSON));
cachedResourceClient.fetchResource();
cachedResourceClient.fetchResource();
server.verify();
}
}
As you can see, we're calling the fetchResource method two times. But the server.verify call fails, because there are also two requests sent to the mock server.
I think the RestClientTest does something behind the scene with my CachedResourceClient that conflicts with the way Caching is setup.
Because if I remove the RestClientTest-Annotation and manually mock the RestTemplateBuilder and the RestTemplate the tests work as expected.
But having a server mock is a bit more comfortable than manually mocking the used classes.
Is there something wrong with the setup?

Related

How to create RestTemplate service for multiple base URLs

I want to implement RestTemplate service in Spring Boot that can work for multiple base URLs.
Here is my service class:
#Service
public class MyService {
private String baseUrl;
#Autowired
private RestTemplate restTemplate;
public ResponseEntity<String> method1() {...}
public ResponseEntity<String> method2() {...}
}
I am thinking how to inject that baseUrl when calling my service methods. Maybe I can create MyService constructor and get method to create new MyService instance:
public MyService(String baseUrl) {
this.baseUrl = baseUrl;
}
public static MyService get(String baseUrl) {
return new MyService(baseUrl);
}
But using this approach I will not be able to get RestTemplate autowired correctly.
So I am wondering what would be the best approach to solve my problem and not to create other classes?
You could skip setting baseUrl for RestTemplate and instead provide full url when doing requests:
var restTemplate = new RestTemplate();
restTemplate.exchange("https://google.pl", HttpMethod.GET, null, String.class);
Url's can be provided when constructing object, by creating a #Configuration class containing MyService definition:
#Bean
public RestTemplate restTemplate() {
// Creating rest template
}
#Bean
public MyService restTemplate() {
// var baseUrls = ...
// return new MyService(baseUrls, restTemplate())
}
My recommendation would be to change approach, assuming each baseUrl is a completely different external service, it would be best to inject multiple RestTemplate, each containing it's own baseUrl.
Additionally if possible, consider moving away from RestTemplate in favour of WebClient or OpenFeign, as RestTemplate is in maintenance mode:
<p><strong>NOTE:</strong> As of 5.0 this class is in maintenance mode, with
only minor requests for changes and bugs to be accepted going forward. Please,
consider using the {#code org.springframework.web.reactive.client.WebClient}
which has a more modern API and supports sync, async, and streaming scenarios.
Source: https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java

I keep getting 401 [No Body response] using RestTemplateBuilder

I have two RestTemplate Beans, however sometimes when authRestTemplate is used get a 401 error (org.springframework.web.client.HttpClientErrorException$Unauthorized: 401 : [no body]) I'm not sure why this is happening since it does not happen consistently.
This is how I have configured my RestTemplate.
#Configuration
#RequiredArgsConstructor
public class AppConfig {
private final FooConfig fooConfig;
#Bean
#LoadBalanced
public RestTemplate authRestTemplate(final RestTemplateBuilder restTemplateBuilder){
return restTemplateBuilder
.basicAuthentication(fooConfig.getUsername(), fooConfig.getPassword())
.build();
}
#Bean
#LoadBalanced
public RestTemplate basicRestTemplate(final RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder
.build();
}
}
If I am using the authRestTemplate it would look something like this
#Service
#RequiredArgsConstructor
public class authSubmissionServiceImpl implements CompletedApplicationService {
private final RestTemplate authRestTemplate;
private final FooConfig fooConfig;
#Override
public void doSomething() {
try {
Objects.requireNonNull(
authRestTemplate.getForObject(
String.format(somePath, fooConfig.getBaseUrl()),
String.class)));
} catch (Exception e) {
e.printStackTrace();
}
}
}
I use constructor injection and use the same variable name as the RestTemplkete bean to differentiate between the two. Because of this, I don't need to use #Qualifier(). However, I know the authentication is being sent because 99% of the time the request goes through fine but there are spontaneous times where I get 401 [No Body] and I'm stumped. Any help would be greatly appreciated

How to test a Java/Spring service using RestTemplate without calling external API and by using Dependency Injection?

In my Spring application, i have a service MyService. MyService calls an external API, counts the products there and returns the result. To call that API it uses the Spring module RestTemplate. To inject the RestTemplate it is configured as #Bean in the DependencyConfig:
#Service
public class ServiceImpl implements MyService {
private final RestTemplate restTemplate;
public ServiceImpl(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
#Override
public String serve() {
ShopProductsResponse result = restTemplate.getForObject(
"https://api.predic8.de/shop/products/",
ShopProductsResponse.class
);
if (result == null) {
return "Error occurred";
}
return String.format("Found %s products", result.getProducts().length);
}
}
Now i want to test it, without calling the external API. So i inject the MyService via #Autowired and the RestTemplate via #MockBean. To define the result of restTemplate.getForObject(...) i use the Mockito.when(...).thenReturn(...) method to mock the result:
#SpringBootTest
class ServiceTest {
#MockBean
private RestTemplate restTemplate;
#Autowired
private MyService service;
#BeforeEach
void init() {
final ShopProductsResponse response = new ShopProductsResponse();
response.setProducts(new Product[0]);
Mockito.when(restTemplate.getForObject(Mockito.any(), Mockito.any())).thenReturn(response);
}
#Test
void test() {
final String expectation = "Found 0 products";
String result = service.serve();
Mockito.verify(restTemplate, Mockito.times(1)).getForObject(
ArgumentMatchers.eq("https://api.predic8.de/shop/products/"),
ArgumentMatchers.eq(ShopProductsResponse.class)
);
Assertions.assertEquals(expectation, result);
}
}
Problem is, that the result from restTemplate.getForObject(...) is null, so that the test fails with the message
org.opentest4j.AssertionFailedError:
Expected :Found 0 products
Actual :Error occurred
So my question is, what am i doing wrong? I thought i am telling the mock what to return. How would i do it correct?
If someone wants to try it out i pushed the example project to Github: https://github.com/roman-wedemeier/spring_example
Trying to find an answer in the net it was quite confusing to me, that there are different versions of Junit(4/5). Also somewhere i found a tutorial about mocking the service directly, which is not what i want to do. On the other hand someone explained how to mock the service's dependency but without using Spring's dependency injection.
restTemplate.getForObject() method has multiple set of parameters which can be invoked with:
restTemplate.getForObject(String url, Class<T> responseType, Object... uriVariables)
restTemplate.getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables)
restTemplate.getForObject(URI url, Class<T> responseType)
so you have probably mocked another method call by providing the widest matchers (Mockito.any()). I suggest you to try mock the restTemplate.getForObject() method call by providing more specific matchers, like for example:
Mockito.when(restTemplate.getForObject(Mockito.anyString(), Mockito.any())).thenReturn(response);
and the test should pass successfully.
Additionally, you can use just Mockito and DI to make unit tests for the the service instead of setting up the whole Spring application context (done by #SpringBootTest annotation). It is not necessary here and it only make the tests last much longer. Here is the alternative way to implement your test:
class ServiceTest {
private RestTemplate restTemplate;
private MyService service;
#BeforeEach
void init() {
final ShopProductsResponse response = new ShopProductsResponse();
response.setProducts(new Product[0]);
this.restTemplate = Mockito.mock(RestTemplate.class);
Mockito.when(this.restTemplate.getForObject(Mockito.anyString(), Mockito.any())).thenReturn(response);
this.service = new ServiceImpl(restTemplate);
}
#Test
void test() {
final String expectation = "Found 0 products";
String result = service.serve();
Mockito.verify(restTemplate, Mockito.times(1)).getForObject(
ArgumentMatchers.eq("https://api.predic8.de/shop/products/"),
ArgumentMatchers.eq(ShopProductsResponse.class)
);
Assertions.assertEquals(expectation, result);
}
}

Spring boot testing of a rest client using #RestClientTest

I am using spring boot 1.5.8 and want to test my client:
#Component
public class RestClientBean implements RestClient {
private Map<String, RestTemplate> restTemplates = new HashMap<>();
#Autowired
public RestClientBean(RestTemplateBuilder builder, SomeConfig conf) {
restTemplates.put("first", builder.rootUri("first").build();
restTemplates.put("second", builder.rootUri("second").build();
}
}
with the following test:
#RunWith(SpringRunner.class)
#RestClientTest(RestClient.class)
public class RestClientTest {
#Autowired
private RestClient client;
#Autowired
private MockRestServiceServer server;
#TestConfiguration
static class SomeConfigFooBarBuzz {
#Bean
public SomeConfig provideConfig() {
return new SomeConfig(); // btw. not sure why this works,
// but this is the only way
// I got rid of the "unable to load
// SomeConfig auto-wire" or something like this :)
// Anyway not my main issue
// EDIT: just realized that the whole
// #TestConfiguration part can be avoided by
// adding SomeConfig.class to the classes in the
// #RestClientTest annotation
}
}
#Before
public void setUp() throws Exception {
server.expect(requestTo("/somePath")) // here an exception is thrown
// (main issue)
.andRespond(withSuccess("<valid json>", MediaType.APPLICATION_JSON));
}
}
The exception is very clear:
java.lang.IllegalStateException: Unable to use auto-configured
MockRestServiceServer since MockServerRestTemplateCustomizer has been bound to
more than one RestTemplate
But is it somehow possible to get this tested or is it not allowed to instantiate two different rest templates in one client class?
I have just the need to use the first rest template in some cases and the second one in some others.
After some days of investigation and communication with spring folks via GitHub I found a solution for me and not receiving an answer here means my solution might be valuable for someone:
#RunWith(SpringRunner.class)
#RestClientTest(RestClient.class)
public class RestClientTest {
#Autowired
private RestClient client;
private MockRestServiceServer firstServer;
private MockRestServiceServer secondServer;
private static MockServerRestTemplateCustomizer customizer;
#TestConfiguration
static class RestTemplateBuilderProvider {
#Bean
public RestTemplateBuilder provideBuilder() {
customizer = new MockServerRestTemplateCustomizer();
return new RestTemplateBuilder(customizer);
}
}
#Before
public void setUp() {
Map<RestTemplate, MockRestServiceServer> servers = customizer.getServers();
// iterate and assign the mock servers according to your own strategy logic
}
#Test
public void someTest() {
firstServer.expect(requestTo("/somePath"))
.andRespond(withSuccess("some json body"),
MediaType.APPLICATION_JSON));
// call client
// verify response
}
Basically specify a number of mock servers same as the number of rest templates you use in your client code, then specify a test configuration providing a rest builder with a customizer, so that your client code's rest templates will be built via this customized builder. Then use the customizer to get the mock servers bounded to the created rest templates and define expectations on them as you want.

How to mock RestTemplate in Java Spring?

public class ServiceTest {
#Mock
RestTemplate restTemplate = new RestTemplate();
#InjectMocks
Service service = new Service();
ResponseEntity responseEntity = mock(ResponseEntity.class);
#Test
public void test() throws Exception {
Mockito.when(restTemplate.getForEntity(
Mockito.anyString(),
Matchers.any(Class.class)
))
.thenReturn(responseEntity);
boolean res = service.isEnabled("something");
Assert.assertEquals(res, false);
}
I tried to test a simple test for a service including a restclient. It looks I have not Mock the RestTemplate successfully. It looks like the code get the real data not the mock one. Anyone can help me with this.
The service itself will looks as this:
public class Service{
public boolean isEnabled(String xxx) {
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.getForEntity("someurl",String.class);
if(...)return true;
return false;
}
}
The problem is that in your isEnabled you are creating a new RestTemplate. This is wrong for two reasons, one is that you cannot mock it since you are creating a new one, and second it is good to avoid creating new objects per request. RestTemplate is thread safe and hence can be a service class member, being used across many threads.
Change your service class to something like this:
public class Service{
RestTemplate restTemplate = new RestTemplate();
public boolean isEnabled(String xxx) {
ResponseEntity<String> response = restTemplate.getForEntity("someurl",String.class);
if(...)return true;
return false;
}
}
Now that your RestTemplate has become a class member you can now properly mock through one of two ways. One, inject it using the #InjectMock, or use a setter method that you call from your test.
Since you are using InjectMock in your code we can go with that.
#RunWith(MockitoJUnitRunner.class)
public class ServiceTest {
#Mock
RestTemplate restTemplate;
#InjectMocks
#Spy
Service service;
ResponseEntity responseEntity = mock(ResponseEntity.class);
#Test
public void test() throws Exception {
Mockito.when(restTemplate.getForEntity(
Mockito.anyString(),
ArgumentMatchers.any(Class.class)
))
.thenReturn(responseEntity);
boolean res = service.isEnabled("something");
Assert.assertEquals(res, false);
}
Notice that I made a few changes. First, I removed the new RestTemplate() and new Service(). You should let mockito create those for you. By annotating them with #Mock and #Spy you will ensure that Mockito will create them for you, and more importantly, will inject the mocks into your service object.
Spring MVC's test framework has offered the class MockRestServiceServer for unit testing RESTful service code.
Here is a tutorial on its use.
If you use #Autowired, you could use MockRestServiceServer.
The below is the sample.
#Service
public class Service{
#Autowired
private RestTemplate restTemplate;
public boolean isEnabled(String xxx) {
ResponseEntity<String> response = restTemplate.getForEntity("someurl",String.class);
if(...)return true;
return false;
}
}
#Service needs to use #Autowired for creating object automatically.

Categories

Resources