0

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.

pixel
  • 9,653
  • 16
  • 82
  • 149
  • 1
    Could you post the stack trace? My guess - null uri in system under test – Lesiak Nov 17 '21 at 22:00
  • Yes, so I created setter for the URI; however, this is supposed to be unit test, not integration test as I discovered once I did that. So, the call to `carServiceSut.getCar` above then requires real uri, real endpoint, real authentication, real database which is very bad. I want this mocked and it looks like mockito and restTemplate do not work together properly. I dont want to call a real endpoint, I want it all to be mocked – pixel Nov 17 '21 at 23:52

2 Answers2

2

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?

Lesiak
  • 22,088
  • 2
  • 41
  • 65
  • thank you. I figured the thing about the uri being null but my question is about mocking it. I injected RestTemplate with `@Mock` and `@InjectMock` (pleasee see UPDATE 1 section); however, even though it is all supposed to be mocked, it looks like it still wants to access real uri (some dummy url will fail), requires real authentication credentials (dummy ones cause it to fail) etc. So it requires *real* backend, *real* authentication creds, *real* url meaning it is not a unit test but integration test, and definitelly not a mock. That is my problem – pixel Nov 18 '21 at 23:28
  • ... in other workds, in your answer, your CarServiceTest, requires YOUR_URI to be a real URI and will fetch real data which is what I do not want, that is no longer unit test but integration test – pixel Nov 18 '21 at 23:30
  • 1
    No, this is not the case. YOUR_URI can be any string, and CarServiceImpl interacts with restTemplateMock, which is a mock. – Lesiak Nov 18 '21 at 23:36
  • hmmm, I tried it just yesterday and could only get it work unless I provide real url, real credentials . I will try using same setup as you suggested to use `Mockito.mock()` instead of using `@Mock` and `@InjectMock` as you suggested. Maybe that is the reason I dont see it working properly – pixel Nov 18 '21 at 23:42
  • 1
    Ah, now I see: you construct RestTemplate restTemplate = new RestTemplate(); in your method instead of injecting it. Let me update my answer – Lesiak Nov 18 '21 at 23:44
  • sorry, I am not following your last two sentences in updated answer, looks like they conflict with suggested solution – pixel Nov 18 '21 at 23:59
  • I meant that now RestTemplate is not created by method under test, and in production code, Spring will need to inject it into your bean. Thus, it must have it registered. “If needed” means “if not provided by auto-configuration”. I mentioned thread safety as previously you had one RestTemplate per call. And it does not conflict in the solution - in test, you provide a mock via constructor, Spring DI is not used. – Lesiak Nov 19 '21 at 00:06
  • I understand how DI works but I do not know how to "register". Would you mind providing that information in your answer as well and update it. Thank you. Because that is exactly what I am running into issues now, I no longer create RestTemplate neither in prod code nor test but now my endpoint no longer works. In other words, my test now works as desired, but my code no longer works because now in production, my restTemplate is nULL – pixel Nov 19 '21 at 00:11
  • 1
    See: https://docs.spring.io/spring-javaconfig/docs/1.0.0.m3/reference/html/creating-bean-definitions.html You need `@Configuration` class, providing` @Bean` RestTemplate – Lesiak Nov 19 '21 at 00:19
  • I just added `@Bean public RestTemplate getRestTemplate() { return new RestTemplate; }` in my SpringbootApplication and then Autowired it in prod code and now both prod and test are working. Much appreciated!!! – pixel Nov 19 '21 at 00:22
  • ah, I see you updated that in your answer. Thanks again!! – pixel Nov 19 '21 at 00:23
0

try to change the URI as

String uri = "http://some/fake/url";
newcoder
  • 464
  • 9
  • 23