I am trying to write integration test in Spring Boot 2.7.3.
This application should scrape data from some external REST service. To call rest service I am using RestTemplate from spring framework
#Service
public class UserService {
private final RestTemplate restTemplate;
private String url = "http://localhost:3000/user/{name}";
#Autowired
public UserService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public UserData getUserInfo(UserInfo userInfo) {
return restTemplate.getForObject(url, UserData.class, userInfo.getName());
}
}
This service is used by rest controller that is accepting name as a path parameter (we can call it localhost:8080/info?name=joe)
#RestController
public class UserController {
private UserService userService;
#Autowired
public UserController(UserService userService) {
this.userService = userService;
}
#GetMapping("/info")
#ResponseBody
public UserData getUserData(UserInfo userInfo) {
return userService.getUserInfo(userInfo);
}
}
I have created controller annotated with #RestControllerAdvice and handling method that should be invoked when throwing HttpClientErrorException
#RestControllerAdvice
public class UserExceptionHandler {
#Autowired
public UserExceptionHandler(){
}
#ExceptionHandler(value = {HttpClientErrorException.class})
public ErrorResponse clientErrorHandle(HttpClientErrorException e) {
return ErrorResponse.builder().error("could not retrieve user info").build();
}
}
Problem is that below test is not hitting UserExceptionHandler
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserControllerTest {
#Autowired
RestTemplate restTemplate;
#Autowired
UserController userController;
private MockRestServiceServer mockServer;
#BeforeEach
public void setUp() {
mockServer = MockRestServiceServer.createServer(restTemplate);
}
#Test
public void testGetRootResourceOnce() {
mockServer.expect(once(), requestTo("http://localhost:3000/user/joe"))
.andRespond(withBadRequest().contentType(MediaType.APPLICATION_JSON).body("{\"error\": \"Error while getting data\"}"));
UserInfo userInfo = UserInfo.builder().name("joe").build();
UserData userData = userController.getUserData(userInfo);
mockServer.verify();
assertThat(userData).isNotNull();
}
}
While debugging I set breakpoint on UserExceptionHandler constructor so I assume that it has been created.
But why method clientErrorHandle was not invoked?
I see that RestTemplate throw HttpClientErrorException (please see picture below) so it should be intercepted by my handler, but it was not
Related
I need to write unit tests for a Spring Controller class.
The setup is like this:
#RestController
#RequestMapping("/")
public class MyCustomController {
#Autowired
private MagicWriter magicWriter;
#Autowired
private MagicUpdater magicUpdater;
#RequestMapping(path = "/", method = RequestMethod.POST)
public String postMagicMethod(#RequestParam(name = "SomeParam") String param1) {
var magicHandler = new MagicHandler(magicWriter, magicUpdater);
return magicHandler.doSomeMagic();
}
}
From my JUnit test, I need to use #MockBean for magicWriter and magicUpdater class.
So far I could not find anything constructive.
Here is my Unit test
#SpringJUnitConfig
#WebMvcTest(value= MyCustomController.class)
public class MyCustomControllerTest {
#Autowired
private MockMvc mockMvc;
#MockBean
private MagicWriter magicWriter;
#MockBean
private MagicUpdater magicUpdater;
#Autowired
private WebApplicationContext webApplicationContext;
#Configuration
static class Config {
#Bean
MyCustomController dispatchController() {
return new MyCustomController();
}
}
#Test
void basicTest() throws Exception {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
HttpHeaders headers = new HttpHeaders();
// Added some http headers
String uri = "/";
RequestBuilder request = MockMvcRequestBuilders.request(HttpMethod.POST, uri, headers);
MvcResult result = mockMvc.perform(request).andReturn();
assertThat(result.getResponse().getContentAsString()).isEqualTo(expected);
}
}
Convert your #Autowired parameters to be constructor based and not field-based.
#RestController
#RequestMapping("/")
public class MyCustomController {
private MagicWriter magicWriter;
private MagicUpdater magicUpdater;
#Autowired
public MyCustomController(MagicWriter magicWriter, MagicUpdater magicUpdater) {
this.magicWriter = magicWriter;
this.magicUpdater = magicUpdater;
}
// ... rest of your code
}
Then in your test, you just new an instance of this class with your mocks passed in. You're already resigned to using mock beans, so you don't need to whole Spring Context to come along.
// Unit test code example
MyCustomController testObject;
MagicWriter magicWriterMock;
magicUpdater magicUpdaterMock;
#BeforeEach
void setUp() throws Exception {
magicWriterMock = mock(MagicWriter.class);
magicUpdaterMock = mock(MagicUpdater.class);
testObject = new MyCustomController(magicWriterMock, magicUpdaterMock);
}
I have a simple Rest Controller as below
#RestController
public class HealthController {
private static final CustomLogger logger = CustomLogger.getLogger(HealthController.class.getName());
private HealthService healthService;
#Autowired
public HealthController(HealthService healthService) {
this.healthService = healthService;
}
#RequestMapping(value = "/health", method = RequestMethod.GET)
public ResponseEntity<?> healthCheck() {
return healthService.checkHealth();
}
}
The service class is below
#Service
public class HealthService {
private static final CustomLogger logger = CustomLogger.getLogger(HealthController.class.getName());
public ResponseEntity<?> checkHealth() {
logger.info("Inside Health");
if (validateHealth()) {
return new ResponseEntity<>("Healthy", HttpStatus.OK);
} else {
return new ResponseEntity<>("Un Healthy", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
boolean validateHealth() {
return true;
}
}
The corresponding unit test for the controller class as below
#RunWith(SpringRunner.class)
#WebMvcTest(controllers = HealthController.class)
public class HealthControllerTest {
#Autowired
private MockMvc mockMvc;
#MockBean
private HealthService healthService;
#Test
public void checkHealthReturn200WhenHealthy() throws Exception {
ResponseEntity mockSuccessResponse = new ResponseEntity("Healthy", HttpStatus.OK);
when(healthService.checkHealth()).thenReturn(mockSuccessResponse);
RequestBuilder requestBuilder = MockMvcRequestBuilders.get(
"/health").accept(
MediaType.APPLICATION_JSON);
MvcResult healthCheckResult = mockMvc
.perform(requestBuilder).andReturn();
Assert.assertEquals(HttpStatus.OK.value(), healthCheckResult.getResponse().getStatus());
}
}
The problem I have is my CustomLogger. Since it has external dependencies am having issues in trying to test this.The same kind of logger is present in my service classes too.
How can I test such a class. I tried the below stuffs
Created a custom class name CustomLoggerForTest under test. Used
ReflectionTestUtils.setField(healthService, "logger", new CustomerLoggerForTest(HealthService.class.getName()));
in the setUp. But it did not help. Using this we cannot set the static fields hence tried even converting them to be non-static
Tried with mocking the CustomLogger in setup as below
mockStatic(CustomLogger.class); when(CustomLogger.getLogger(any())) .thenReturn(new CustomLoggerForTest(HealthController.class.getName()));
But no luck.
Is there anything that am doing wrong that is causing this?
Currently struggling with problem when I get 'mapping error for request' with following controller/test configuration.
Controller:
#Slf4j
#Validated
#RestController
#RequiredArgsConstructor
public class AdtechController {
private final AdtechService adtechService;
#PostMapping(value = "/subscriber/session")
public ResponseEntity<ResponseDto> submitSession(#RequestBody RequestDto requestDto) {
log.trace("execute submitSession with {}", requestDto);
ResponseDtoresponse = adtechService.submitSession(requestDto);
return new ResponseEntity<>(response, HttpStatus.OK);
}
#ExceptionHandler(AdtechServiceException.class)
public ResponseEntity<AdtechErrorResponse> handleAdtechServiceException(AdtechServiceException e) {
return new ResponseEntity<>(new AdtechErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Test:
#RunWith(SpringRunner.class)
#SpringBootTest
#AutoConfigureMockMvc
#SpringJUnitConfig({AdtechTestConfig.class})
public class AdtechControllerTest {
private static final ObjectMapper OBJECT_MAPPER = JsonUtil.getJackson();
#Autowired
private MockMvc mockMvc;
#Test
public void testSubmitSession() throws Exception {
RequestDto requestDto = new RequestDto ();
requestDto.setKyivstarId("1123134");
requestDto.setMsisdn("123476345242");
requestDto.setPartnerId("112432523");
requestDto.setPartnerName("125798756");
String request = OBJECT_MAPPER.writeValueAsString(requestDto);
System.out.println("REQUEST: " + request);
String response = OBJECT_MAPPER.writeValueAsString(new ResponseDto("123"));
System.out.println("RESPONSE: " + response);
mockMvc.perform(post("/subscriber/session")
.content(MediaType.APPLICATION_JSON_VALUE)
.content(request))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string(containsString(response)));
}
}
Configuration:
#Configuration
public class AdtechTestConfig {
#Bean
public AdtechService adtechTestService() {
return requestDto -> new AdtechResponseDto("123");
}
}
After test execution I get No mapping for POST /subscriber/session
The reason for the struggle is that my code from other modules with the same configuration works fine. Can somebody point out what am I missing ? Thanks in advance!
Apparently you are loading a configuration class to mock beans, this interferes with the other parts of Spring Boot and probably leads to partially loading your application. I suspect only the mocked service is available.
Instead of the test configuration use #MockBean to create a mock for the service and register behaviour on it.
#SpringBootTest
#AutoConfigureMockMvc
public class AdtechControllerTest {
private static final ObjectMapper OBJECT_MAPPER = JsonUtil.getJackson();
#Autowired
private MockMvc mockMvc;
#MockBean
private AdtechService mockService;
#BeforeEach
public void setUp() {
when(mockService.yourMethod(any()).thenReturn(new AdtechResponseDto("123"));
}
#Test
public void testSubmitSession() throws Exception {
// Your original test method
}
}
If the only thing you want to test is your controller you might also want to consider using #WebMvcTest instead of #SpringBootTest.
#WebMvcTest(AdTechController.class)
public class AdtechControllerTest {
private static final ObjectMapper OBJECT_MAPPER = JsonUtil.getJackson();
#Autowired
private MockMvc mockMvc;
#MockBean
private AdtechService mockService;
#BeforeEach
public void setUp() {
when(mockService.yourMethod(any()).thenReturn(new AdtechResponseDto("123"));
}
#Test
public void testSubmitSession() throws Exception {
// Your original test method
}
}
This will load a scaled-down version of the context (only the web parts) and will be quicker to run.
try this:
#Slf4j
#Validated
#RestController
#RequiredArgsConstructor
public class AdtechController {
private AdtechService adtechService;
public AdtechController (AdtechService adtechService) {
this.adtechService= adtechService;
}
#PostMapping(value = "/subscriber/session")
public ResponseEntity<ResponseDto> submitSession(#RequestBody RequestDto requestDto) {
log.trace("execute submitSession with {}", requestDto);
ResponseDtoresponse = adtechService.submitSession(requestDto);
return new ResponseEntity<>(response, HttpStatus.OK);
}
#ExceptionHandler(AdtechServiceException.class)
public ResponseEntity<AdtechErrorResponse> handleAdtechServiceException(AdtechServiceException e) {
return new ResponseEntity<>(new AdtechErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Test:
#RunWith(SpringRunner.class)
#SpringBootTest
#AutoConfigureMockMvc
#SpringJUnitConfig({AdtechTestConfig.class})
public class AdtechControllerTest {
private static final ObjectMapper OBJECT_MAPPER = JsonUtil.getJackson();
#Autowired
private MockMvc mockMvc;
#Autowired
private AdtechService adtechService;
#Before
public void setUp() {
MockitoAnnotations.initMocks(this);
this.mvc = MockMvcBuilders.standaloneSetup(new AdtechController(adtechService)).build();
}
#Test
public void testSubmitSession() throws Exception {
RequestDto requestDto = new RequestDto ();
requestDto.setKyivstarId("1123134");
requestDto.setMsisdn("123476345242");
requestDto.setPartnerId("112432523");
requestDto.setPartnerName("125798756");
String request = OBJECT_MAPPER.writeValueAsString(requestDto);
System.out.println("REQUEST: " + request);
String response = OBJECT_MAPPER.writeValueAsString(new ResponseDto("123"));
System.out.println("RESPONSE: " + response);
mockMvc.perform(post("/subscriber/session")
.content(MediaType.APPLICATION_JSON_VALUE)
.content(request))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string(containsString(response)));
}
}
Is the AdtechTestConfig.class introducing the /ad-tech path segment in to your test request? If so, this is why your test is trying the path /ad-tech/subscriber/session instead of /subscriber/session.
If this is actually the correct uri, then you may add #RequestMapping to the controller like below or just to the post method itself
#Slf4j
#Validated
#RestController
#RequestMapping("/ad-tech")
#RequiredArgsConstructor
public class AdtechController {
private final AdtechService adtechService;
#PostMapping(value = "/subscriber/session")
public ResponseEntity<ResponseDto> submitSession(#RequestBody RequestDto requestDto) {
...
I'm writing an Spring MVC integration test and want to mock an external service, which is embedded within the class structure. However, I can't seem to get the mock to work.
This is my class structure:
Controller:
#RestController
public class Controller {
private final MyService service;
#Autowired
public Controller(MyService service) {
this.service = service;
}
#RequestMapping(value = "/send", method = POST, produces = APPLICATION_JSON_VALUE)
public void send(#RequestBody Response response) {
service.sendNotification(response);
}
}
Service:
#Service
public class MyService {
// Client is external service to be mocked
private final Client client;
private final Factory factory;
#Autowired
public MyService(Client client, Factory factory) {
this.factory = factory;
this.client = client;
}
public void sendNotification(Response response) {
// Implemented code some of which will be mocked
}
}
Integration Test:
#RunWith(SpringRunner.class)
#SpringBootTest
#AutoConfigureMockMvc
public class IntegrationTest {
MockMvc mockMvc;
#Autowired
MyService service;
#Mock
Client client;
#Autowired
Factory factory;
#InjectMocks
Controller controller;
#Before
public void setup() {
initMocks(this);
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
}
#Test
public void test1() {
String json = "{/"Somejson/":/"test/"}";
mockMvc.perform(post("/send")
.contentType(MediaType.APPLICATION_JSON)
.content(json))
.andExpect(status().isCreated());
}
}
This results in the service ending up as null. Can anyone spot what I am doing wrong? Thanks
Good thing is you are using constructor Injection in Controller and Service class. Which makes it easier to initialize with mocks
This should work.
public class IntegrationTest {
MockMvc mockMvc;
MyService service;
Controller controller;
#Mock
Client client;
#Autowired
Factory factory;
#Before
public void setup() {
initMocks(this);
MyService service = new MyService(client, factory);
controller = new Controller(service);
this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
}
I'm trying to test my Rest controllers from my Spring Boot application and want the controllers to be available under the same path as in production.
For example I have the following Controller:
#RestController
#Transactional
public class MyController {
private final MyRepository repository;
#Autowired
public MyController(MyRepository repository) {
this.repository = repository;
}
#RequestMapping(value = "/myentity/{id}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public ResponseEntity<Resource<MyEntity>> getMyEntity(
#PathVariable(value = "id") Long id) {
MyEntity entity = repository.findOne(id);
if (entity == null) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<>(entity, HttpStatus.OK);
}
}
Within my application.yml I have configured the context path for the application:
server:
contextPath: /testctx
My test for this controller looks as follows:
#RunWith(SpringRunner.class)
#WebMvcTest(controllers = MyController.class, secure=false)
public class MyControllerTest {
#Autowired
private MyRepository repositoryMock;
#Autowired
private MockMvc mvc;
#Test
public void testGet() throws Exception {
MyEntity entity = new MyEntity();
entity.setId(10L);
when(repositoryMock.findOne(10L)).thenReturn(entity);
MockHttpServletResponse response = this.mvc.perform(
MockMvcRequestBuilders.get("/testctx/myentity/10"))
.andReturn().getResponse();
assertEquals(response.getStatus(), 200);
}
#TestConfiguration
public static class TestConfig {
#Bean
MyRepository mockRepo() {
return mock(MyRepository.class);
}
}
}
This test fails since the status code is 404 for the call. If I call /myentity/10 it works. Unfortunately the rest call is initiated by a CDC-Test-Framework (pact) so I cannot change the requested path (containing the context path /testctx). So is there a way to tell spring boot test to start the rest endpoint with a defined context path also during testing?
You could try:
#WebMvcTest(controllers = {MyController.class})
#TestPropertySource(locations="classpath:application.properties")
class MyControllerTest {
#Autowired
protected MockMvc mockMvc;
#Value("${server.servlet.context-path}")
private String contextPath;
#BeforeEach
void setUp() {
assertThat(contextPath).isNotBlank();
((MockServletContext) mockMvc.getDispatcherServlet().getServletContext()).setContextPath(contextPath);
}
protected MockHttpServletRequestBuilder createGetRequest(String request) {
return get(contextPath + request).contextPath(contextPath)...
}