如何在不调用外部 API 和使用依赖注入的情况下使用 RestTemplate 测试 Java/Spring 服务?

How to test a Java/Spring service using RestTemplate without calling external API and by using Dependency Injection?

在我的 Spring 应用程序中,我有一项服务 MyServiceMyService 调用外部 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);
    }

}