Search code examples
spring-bootunit-testingmockitoresttemplate

Spring Mockito test of RestTemplate.postForEntity throws IllegalArgumentException: URI is not absolute


My Controller calls the service to post information about a car like below and it works fine. However, my unit test fails with the IllegalArgumentException: URI is not absolute exception and none of the posts on SO were able to help with it.

Here is my controller

@RestController
@RequestMapping("/cars")  
public class CarController {

    @Autowired
    CarService carService;

    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<CarResponse> getCar(@RequestBody CarRequest carRequest, @RequestHeader HttpHeaders httpHeaders) {

        ResponseEntity<CarResponse> carResponse = carService.getCard(carRequest, httpHeaders);

        return carResponse;
    }
}

Here is my service class:

@Service
public class MyServiceImpl implements MyService {

    @Value("${myUri}")
    private String uri;

    public void setUri(String uri) { this.uri = uri; }

    @Override
    public ResponseEntity<CarResponse> postCar(CarRequest carRequest, HttpHeaders httpHeaders) {
        List<String> authHeader = httpHeaders.get("authorization");

        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", authHeader.get(0));

        HttpEntity<CarRequest> request = new HttpEntity<CarRequest>(carRequest, headers);

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<CarResponse> carResponse = restTemplate.postForEntity(uri, request, CarResponse.class);

        return cardResponse;
    }
}

However, I am having trouble getting my unit test to work. The below tests throws IllegalArgumentException: URI is not absolute exception:

public class CarServiceTest {

    @InjectMocks
    CarServiceImpl carServiceSut;

    @Mock
    RestTemplate restTemplateMock;

    CardResponse cardResponseFake = new CardResponse();

    @BeforeEach
    void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);

        cardResponseFake.setCarVin(12345);
    }

    @Test
    final void test_GetCars() {
        // Arrange
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", anyString());

        ResponseEntity<CarResponse> carResponseEntity = new ResponseEntity(carResponseFake, HttpStatus.OK);

        String uri = "http://FAKE/URI/myapi/cars";
        carServiceSut.setUri(uri);

        when(restTemplateMock.postForEntity(
            eq(uri), 
            Mockito.<HttpEntity<CarRequest>> any(), 
            Mockito.<Class<CarResponse>> any()))
        .thenReturn(carResponseEntity);

          // Act
          **// NOTE: Calling this requires real uri, real authentication,
          // real database which is contradicting with mocking and makes
          // this an integration test rather than unit test.**
        ResponseEntity<CarResponse> carResponseMock = carServiceSut.getCar(carRequestFake, headers); 

        // Assert
        assertEquals(carResponseEntity.getBody().getCarVin(), 12345);
    }
}

UPDATE 1

I figured out why the "Uri is not absolute" exection is thrown. It is because in my carService above, I use @Value to inject uri from application.properties file, but in unit tests, that is not injected.

So, I added public property to be able to set it and updated the code above, but then I found that the uri has to be a real uri to a real backend, requiring a real database.

In other words, if the uri I pass is a fake uri, the call to carServiceSut.getCar above, will fail which means this turns the test into an integration test.

This contradicts with using mocking in unit tests. I dont want to call real backend, the restTemplateMock should be mocked and injected into carServiceSut since they are annotated as @Mock and @InjectMock respectively. Therefore, it whould stay a unit test and be isolated without need to call real backend. I have a feeling that Mockito and RestTemplate dont work well together.


Solution

  • You need to construct your system under test properly. Currently, MyServiceImpl.uri is null. More importantly, your mock of RestTemplate is not injected anywhere, and you construct a new RestTemplate in method under test.

    As Mockito has no support for partial injection, you need to construct the instance manually in test.

    I would:

    Use constructor injection to inject both restTemplate and uri:

    @Service
    public class MyServiceImpl implements MyService {
       
        private RestTemplate restTemplate;
        private String uri;
        
        public MyServiceImpl(RestTemplate restTemplate, @Value("${myUri}") uri) {
            this.restTemplate = restTemplate;
            this.uri = uri;
        }
    

    Construct the instance manually:

    • drop @Mock and @InjectMocks
    • drop Mockito.initMocks call
    • use Mockito.mock and constructor in test
    public class CarServiceTest {
    
        public static String TEST_URI = "YOUR_URI";
    
        RestTemplate restTemplateMock = Mockito.mock(RestTemplate.class);
    
        CarServiceImpl carServiceSut = new CarServiceImpl(restTemplateMock, TEST_URI):
    
    }
    

    Remove creation of restTemplate in method under test.

    If needed, add a config class providing RestTemplate bean (for the application, the test does not need that):

    @Configuration
    public class AppConfig {
        @Bean
        public RestTemplate restTemplate() {
            return new RestTemplate();
        }
    }
    

    Note that RestTemplate is thread-safe, one instance per app is enough: Is RestTemplate thread safe?