Mock RestTemplate.postForEntity时为何报‘URI is not absolute’错误?
我想Mock所属类中RestTemplate实例变量的postForEntity方法,但即便参考示例传入anyString()作为URI,仍持续抛出java.lang.IllegalArgumentException: URI is not absolute错误。需要说明的是,我没有用@Mock注解RestTemplate,因为要测试它是否被类的构造器正确构建(主要是超时配置)。
被测类大致代码如下:
public class MyClient { ... @Getter @Setter private RestTemplate restTemplate = new RestTemplate(); public MyClient(Property x, Property y, Property z){ this.x = x; this.y = y; this.z = z; } // 要测试的新构造器 public MyClient(Property x, Property y, Property z, int timeout){ this(x, y, z); this.restTemplate =((SimpleClientHttpRequestFactory)getRestTemplate().getRequestFactory()).setReadTimeout(timeout); } public void makeRequests() { ... // myEndpoint、myRequest已提前赋值,CustomResponse是自定义类 ResponseEntity resEntity = getRestTemplate().postForEntity(myEndpoint, myRequest, CustomResponse.class); } }
测试类代码:
@RunWith(MockitoJUnitRunner.class) public class MyClassTest extends TestCase{ ... @Test public void test_new_constructor(){ MyClient myClient = new MyClient(x, y, z, 1000); RestTemplate restTemplate = myClient.getRestTemplate(); Mockito.when(restTemplate.postForEntity(anyString(), any(), any(Class.class))).thenAnswer(new AnswersWithDelay(100000, new Returns(new ResponseEntity(CustomResponse, HttpStatus.OK)) )); // 本来打算加延迟触发超时并校验异常,但还没到这一步就报错了 myClient.makeRequests(); } }
疑问:
- 报错的原因是什么?
- 测试实际RestTemplate是不是错误做法?
- 我原本以为用Mockito.when处理网络调用能避免依赖真实端点,但实际并非如此吗?
报错原因
你现在是直接对真实的RestTemplate实例调用Mockito.when(),而不是Mock对象。Mockito的when()方法默认会先执行一次原方法(也就是真实的postForEntity),这时候anyString()会被解析成一个非绝对URI的占位符,触发RestTemplate内部的URI合法性校验,所以抛出了“URI is not absolute”错误。
关于测试真实RestTemplate的合理性
直接测试真实RestTemplate不是错误做法,但你的实现方式不对:你想用Mockito拦截真实对象的方法调用,这需要用Mockito的Spy功能,而不是直接对真实对象用when()。
为什么没避免真实调用?
你以为Mockito.when能避免真实调用,那是针对@Mock创建的完全假的对象。对真实对象用when(),默认会先执行原方法逻辑,所以才会触发真实的URI校验,甚至可能发起真实请求。
修正方案
要同时保留RestTemplate的真实配置(比如你要测试的超时),又能拦截postForEntity的调用,需要把真实RestTemplate包装成Spy对象,并用doAnswer()来设置行为(避免先执行原方法):
@Test public void test_new_constructor(){ MyClient myClient = new MyClient(x, y, z, 1000); // 把真实RestTemplate包装成Spy RestTemplate restTemplateSpy = Mockito.spy(myClient.getRestTemplate()); // 用doAnswer()设置延迟返回,避免先执行原方法 Mockito.doAnswer(new AnswersWithDelay(100000, new Returns(new ResponseEntity<>(new CustomResponse(), HttpStatus.OK)))) .when(restTemplateSpy) .postForEntity(anyString(), any(), any(Class.class)); // 将Spy对象设置回MyClient myClient.setRestTemplate(restTemplateSpy); // 执行方法,此时会触发超时逻辑 try { myClient.makeRequests(); // 如果没抛出超时异常,测试失败 fail("Expected timeout exception"); } catch (ResourceAccessException e) { // 确认是超时导致的异常 assertTrue(e.getCause() instanceof SocketTimeoutException); } }
补充:被测类构造器的小问题
你新构造器里的代码有bug:setReadTimeout是void方法,不能直接赋值给restTemplate,正确写法应该是:
public MyClient(Property x, Property y, Property z, int timeout){ this(x, y, z); SimpleClientHttpRequestFactory factory = (SimpleClientHttpRequestFactory) getRestTemplate().getRequestFactory(); factory.setReadTimeout(timeout); // 不需要重新赋值restTemplate,因为factory是引用类型,修改属性会直接生效 }
内容的提问来源于stack exchange,提问作者Shi Lilian

