Spring RestTemplate.postForEntity 的 Mockito 测试抛出 IllegalArgumentException:URI 不是绝对的

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

我的 Controller 调用该服务以获取有关汽车的 post 信息,如下所示,它工作正常。但是,我的单元测试因 IllegalArgumentException: URI is not absolute 异常而失败,SO 上 post 的 none 能够提供帮助。

这是我的控制器

@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;
    }
}

这是我的服务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;
    }
}

但是,我无法让我的单元测试工作。下面的测试抛出 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);
    }
}

更新 1

我想通了为什么抛出“Uri 不是绝对的”执行。这是因为在我上面的 carService 中,我使用 @Valueapplication.properties 文件中注入 uri,但在单元测试中,没有注入。

所以,我添加了 public 属性 以便能够设置它并更新了上面的代码,但后来我发现 uri 必须是 real uri 到 real 后端,需要 real 数据库。

换句话说,如果我传递的 uri 是假的 uri,上面对 carServiceSut.getCar 的调用将失败,这意味着这会将测试变成集成测试。

这与在单元测试中使用模拟相矛盾。 我不想调用真正的后端,restTemplateMock 应该被模拟并注入到 carServiceSut 中,因为它们分别被注释为 @Mock@InjectMock。因此,它应该保持单元测试并被隔离,而不需要调用真正的后端。我感觉 Mockito 和 RestTemplate 不能很好地协同工作。

尝试将 URI 更改为

String uri = "http://some/fake/url";

您需要正确构建被测系统。 当前,MyServiceImpl.uri 为空。 更重要的是,你mock的RestTemplate并没有被注入到任何地方,你在被测方法中构造了一个新的RestTemplate。

由于Mockito不支持部分注入,需要在测试中手动构建实例

我会:

使用构造函数注入同时注入 restTemplate 和 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;
    }

手动构建实例:

  • 删除@Mock 和@InjectMocks
  • 挂机Mockito.initMocks通话
  • 在测试中使用Mockito.mock和构造函数
public class CarServiceTest {

    public static String TEST_URI = "YOUR_URI";

    RestTemplate restTemplateMock = Mockito.mock(RestTemplate.class);

    CarServiceImpl carServiceSut = new CarServiceImpl(restTemplateMock, TEST_URI):

}

在被测方法中删除 restTemplate 的创建。

如果需要,添加一个配置 class 提供 RestTemplate bean(对于应用程序,测试不需要):

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

注意 RestTemplate 是线程安全的,每个应用一个实例就足够了:Is RestTemplate thread safe?