如何在不调用外部 API 和使用依赖注入的情况下使用 RestTemplate 测试 Java/Spring 服务?
How to test a Java/Spring service using RestTemplate without calling external API and by using Dependency Injection?
在我的 Spring 应用程序中,我有一项服务 MyService
。 MyService
调用外部 API,计算那里的产品并 returns 结果。要调用 API,它使用 Spring 模块 RestTemplate
。要注入 RestTemplate
,它在 DependencyConfig
:
中配置为 @Bean
@Service
public class ServiceImpl implements MyService {
private final RestTemplate restTemplate;
public ServiceImpl(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Override
public String serve() {
ShopProductsResponse result = restTemplate.getForObject(
"https://api.predic8.de/shop/products/",
ShopProductsResponse.class
);
if (result == null) {
return "Error occurred";
}
return String.format("Found %s products", result.getProducts().length);
}
}
现在我想测试它,而不调用外部 API。所以我通过 @Autowired
注入 MyService
,通过 @MockBean
注入 RestTemplate
。要定义 restTemplate.getForObject(...)
的结果,我使用 Mockito.when(...).thenReturn(...)
方法来模拟结果:
@SpringBootTest
class ServiceTest {
@MockBean
private RestTemplate restTemplate;
@Autowired
private MyService service;
@BeforeEach
void init() {
final ShopProductsResponse response = new ShopProductsResponse();
response.setProducts(new Product[0]);
Mockito.when(restTemplate.getForObject(Mockito.any(), Mockito.any())).thenReturn(response);
}
@Test
void test() {
final String expectation = "Found 0 products";
String result = service.serve();
Mockito.verify(restTemplate, Mockito.times(1)).getForObject(
ArgumentMatchers.eq("https://api.predic8.de/shop/products/"),
ArgumentMatchers.eq(ShopProductsResponse.class)
);
Assertions.assertEquals(expectation, result);
}
}
问题是,restTemplate.getForObject(...)
的结果为空,因此测试失败并显示消息
org.opentest4j.AssertionFailedError:
Expected :Found 0 products
Actual :Error occurred
所以我的问题是,我做错了什么?我以为我在告诉模拟 return。我该怎么做才正确?
如果有人想尝试一下,我将示例项目推送到 Github:https://github.com/roman-wedemeier/spring_example
试图在网上找到答案让我很困惑,因为有不同版本的 Junit(4/5)。我也在某个地方找到了一个关于直接模拟服务的教程,这不是我想要做的。另一方面,有人解释了如何在不使用 Spring 的依赖注入的情况下模拟服务的依赖关系。
restTemplate.getForObject()
方法有多个参数集,可以通过以下方式调用:
restTemplate.getForObject(String url, Class<T> responseType, Object... uriVariables)
restTemplate.getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables)
restTemplate.getForObject(URI url, Class<T> responseType)
所以您可能通过提供最广泛的匹配器 (Mockito.any()
) 来模拟另一个方法调用。我建议您通过提供更具体的匹配器来尝试模拟 restTemplate.getForObject()
方法调用,例如:
Mockito.when(restTemplate.getForObject(Mockito.anyString(), Mockito.any())).thenReturn(response);
并且测试应该成功通过。
此外,您可以仅使用 Mockito 和 DI 为服务进行单元测试,而不是设置整个 Spring 应用程序上下文(通过 @SpringBootTest
注释完成)。这里没有必要,它只会使测试持续更长时间。这是实施测试的替代方法:
class ServiceTest {
private RestTemplate restTemplate;
private MyService service;
@BeforeEach
void init() {
final ShopProductsResponse response = new ShopProductsResponse();
response.setProducts(new Product[0]);
this.restTemplate = Mockito.mock(RestTemplate.class);
Mockito.when(this.restTemplate.getForObject(Mockito.anyString(), Mockito.any())).thenReturn(response);
this.service = new ServiceImpl(restTemplate);
}
@Test
void test() {
final String expectation = "Found 0 products";
String result = service.serve();
Mockito.verify(restTemplate, Mockito.times(1)).getForObject(
ArgumentMatchers.eq("https://api.predic8.de/shop/products/"),
ArgumentMatchers.eq(ShopProductsResponse.class)
);
Assertions.assertEquals(expectation, result);
}
}
在我的 Spring 应用程序中,我有一项服务 MyService
。 MyService
调用外部 API,计算那里的产品并 returns 结果。要调用 API,它使用 Spring 模块 RestTemplate
。要注入 RestTemplate
,它在 DependencyConfig
:
@Bean
@Service
public class ServiceImpl implements MyService {
private final RestTemplate restTemplate;
public ServiceImpl(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Override
public String serve() {
ShopProductsResponse result = restTemplate.getForObject(
"https://api.predic8.de/shop/products/",
ShopProductsResponse.class
);
if (result == null) {
return "Error occurred";
}
return String.format("Found %s products", result.getProducts().length);
}
}
现在我想测试它,而不调用外部 API。所以我通过 @Autowired
注入 MyService
,通过 @MockBean
注入 RestTemplate
。要定义 restTemplate.getForObject(...)
的结果,我使用 Mockito.when(...).thenReturn(...)
方法来模拟结果:
@SpringBootTest
class ServiceTest {
@MockBean
private RestTemplate restTemplate;
@Autowired
private MyService service;
@BeforeEach
void init() {
final ShopProductsResponse response = new ShopProductsResponse();
response.setProducts(new Product[0]);
Mockito.when(restTemplate.getForObject(Mockito.any(), Mockito.any())).thenReturn(response);
}
@Test
void test() {
final String expectation = "Found 0 products";
String result = service.serve();
Mockito.verify(restTemplate, Mockito.times(1)).getForObject(
ArgumentMatchers.eq("https://api.predic8.de/shop/products/"),
ArgumentMatchers.eq(ShopProductsResponse.class)
);
Assertions.assertEquals(expectation, result);
}
}
问题是,restTemplate.getForObject(...)
的结果为空,因此测试失败并显示消息
org.opentest4j.AssertionFailedError:
Expected :Found 0 products
Actual :Error occurred
所以我的问题是,我做错了什么?我以为我在告诉模拟 return。我该怎么做才正确?
如果有人想尝试一下,我将示例项目推送到 Github:https://github.com/roman-wedemeier/spring_example
试图在网上找到答案让我很困惑,因为有不同版本的 Junit(4/5)。我也在某个地方找到了一个关于直接模拟服务的教程,这不是我想要做的。另一方面,有人解释了如何在不使用 Spring 的依赖注入的情况下模拟服务的依赖关系。
restTemplate.getForObject()
方法有多个参数集,可以通过以下方式调用:
restTemplate.getForObject(String url, Class<T> responseType, Object... uriVariables)
restTemplate.getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables)
restTemplate.getForObject(URI url, Class<T> responseType)
所以您可能通过提供最广泛的匹配器 (Mockito.any()
) 来模拟另一个方法调用。我建议您通过提供更具体的匹配器来尝试模拟 restTemplate.getForObject()
方法调用,例如:
Mockito.when(restTemplate.getForObject(Mockito.anyString(), Mockito.any())).thenReturn(response);
并且测试应该成功通过。
此外,您可以仅使用 Mockito 和 DI 为服务进行单元测试,而不是设置整个 Spring 应用程序上下文(通过 @SpringBootTest
注释完成)。这里没有必要,它只会使测试持续更长时间。这是实施测试的替代方法:
class ServiceTest {
private RestTemplate restTemplate;
private MyService service;
@BeforeEach
void init() {
final ShopProductsResponse response = new ShopProductsResponse();
response.setProducts(new Product[0]);
this.restTemplate = Mockito.mock(RestTemplate.class);
Mockito.when(this.restTemplate.getForObject(Mockito.anyString(), Mockito.any())).thenReturn(response);
this.service = new ServiceImpl(restTemplate);
}
@Test
void test() {
final String expectation = "Found 0 products";
String result = service.serve();
Mockito.verify(restTemplate, Mockito.times(1)).getForObject(
ArgumentMatchers.eq("https://api.predic8.de/shop/products/"),
ArgumentMatchers.eq(ShopProductsResponse.class)
);
Assertions.assertEquals(expectation, result);
}
}