【问题标题】:Spring Mockito test of RestTemplate.postForEntity throws IllegalArgumentException: URI is not absoluteRestTemplate.postForEntity 的 Spring Mockito 测试抛出 IllegalArgumentException:URI 不是绝对的
【发布时间】:2021-12-28 18:39:35
【问题描述】:

我的控制器调用该服务来发布有关汽车的信息,如下所示,它工作正常。但是,我的单元测试因 IllegalArgumentException: URI is not absolute 异常而失败,并且 SO 上的所有帖子都无法解决此问题。

这是我的控制器

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

这是我的服务类:

@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 异常:

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 is not absolute”执行。这是因为在我上面的carService 中,我使用@Valueapplication.properties 文件中注入uri,但是在单元测试中,并没有注入。

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

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

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

【问题讨论】:

  • 你能发布堆栈跟踪吗?我的猜测 - 被测系统中的 null uri
  • 是的,所以我为 URI 创建了 setter;但是,这应该是单元测试,而不是我发现的集成测试。因此,上面对carServiceSut.getCar 的调用需要真正的uri、真正的端点、真正的身份验证、真正的数据库,这是非常糟糕的。我想要这个嘲笑,看起来 mockito 和 restTemplate 不能正常工作。我不想调用一个真正的端点,我希望这一切都被嘲笑

标签: spring-boot unit-testing mockito resttemplate


【解决方案1】:

尝试将 URI 更改为

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

【讨论】:

  • 谢谢雷,但这不起作用。我修改了我的问题并提供了真实的网址格式
  • 删除 URI uri = new URI("localhost:8080/myapi/cars"),并将其保留为给定的字符串
  • 字符串 uri = "localhost:8080/myapi/cars";试试这个,看看这是否有效
【解决方案2】:

您需要正确构建被测系统。 目前,MyServiceImpl.uri 为空。 更重要的是,您的 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

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

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

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

【讨论】:

  • 谢谢。我想出了关于 uri 为空的事情,但我的问题是关于嘲笑它。我用@Mock@InjectMock 注入了RestTemplate(请参阅更新1 部分);然而,即使这一切都应该被嘲笑,它看起来仍然想要访问真正的 uri(一些虚拟 url 会失败),需要真正的身份验证凭据(虚拟的会导致它失败)等等。所以它需要 real 后端,real 身份验证凭据,real url 意味着它不是单元测试而是集成测试,绝对不是模拟。那是我的问题
  • ... 在其他工作中,在您的回答中,您的 CarServiceTest 要求 YOUR_URI 是一个真实的 URI,并将获取我不想要的真实数据,这不再是单元测试而是集成测试
  • 不,事实并非如此。 YOUR_URI可以是任意字符串,CarServiceImpl与restTemplateMock交互,restTemplateMock是一个mock。
  • 啊,现在我明白了:你构造了 RestTemplate restTemplate = new RestTemplate();在你的方法中而不是注入它。让我更新我的答案
  • 见:docs.spring.io/spring-javaconfig/docs/1.0.0.m3/reference/html/… 你需要@Configuration 类,提供`@Bean` RestTemplate
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-10-28
  • 1970-01-01
  • 2013-02-15
  • 2019-02-19
  • 2013-06-30
  • 1970-01-01
相关资源
最近更新 更多