Mockito 单元测试 RestTemplate

Mockito unit testing RestTemplate

我正在使用 RestTemplate postForEntity 方法将正文 post 发送到端点。我需要帮助使用 Mockito 为我的代码编写测试用例。 return 类型是无效的,但如果需要测试,可以将其更改为 Typescode。我参考了许多其他文档,但它们非常笼统,我尝试使用它们,但大多数对我不起作用,因为 request 和 return 类型不同。 .任何建议表示赞赏。谢谢

这是我的Javaclass

    public void postJson(Set<Type> Types){
        try {
            String oneString = String.join(",", Types);
           Map<String, String> requestBody = new HashMap<>();
            requestBody.put("type", oneString);
            JSONObject jsonObject = new JSONObject(requestBody);
            HttpEntity<String> request = new HttpEntity<String>(jsonObject.toString(), null);
ResponseEntity result = restTemplate.exchange(url, HttpMethod.POST,
                    new HttpEntity<>(request, getHttpHeaders()), String.class);

        } 
    }
} 

不久前,我写了一篇关于 unit testing and test doubles 的文章。您可以阅读作为如何进行单元测试的起点。

它的一些关键要点是:

  • 测试行为而不是实施。独立于实现细节的测试更容易维护。在大多数情况下,测试应该侧重于测试代码的 public API,并且代码的实现细节不需要暴露给测试。
  • 被测单元的大小是任意的,但单元越小越好。
  • 在谈论单元测试时,一个更典型的区别是被测单元应该是社交的还是孤独的。
  • 测试替身是一种可以在测试中代替真实物体的物体,类似于电影中特技替身代替演员的方式。他们是测试双打而不是模拟。模拟是测试替身之一。不同的测试替身有不同的用途。

很难编写完整的测试,因为缺少很多信息。例如。 Type 是什么。 由于您没有发布 class 的名称,我暂时将其命名为 MyClass。 我还假设 RestTemplate 是通过

这样的构造函数注入的
MyClass(RestTemplate restTemplate) {
    this.restTemplate = restTemplate;
}

以下是使用 JUnit 5 and Mockito 进行单元测试的草稿。 我建议嘲笑 RestTemplate。我不得不承认,这样我们就不会在我们的测试中覆盖测试MappingJackson2HttpMessageConverterStringHttpMessageConverter的用法。

所以一个非常原始的草稿可能看起来像这样

import java.util.ArrayList;
import java.util.Set;

import org.junit.Assert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.springframework.http.HttpEntity;
import org.springframework.web.client.RestTemplate;

class MyClassTest {

    private RestTemplate restTemplate;
    private MyClass myClass;

    @BeforeEach
    void setUp() {
        restTemplate = Mockito.mock(RestTemplate.class);
        myClass = new MyClass(restTemplate);
    }

    @Test
    void callMethod() {
        Set<Type> types = Set.of(/* one of your Types */);
        String url = "http://someUrl";
        String httpResult = "";

        Mockito.when(restTemplate.getMessageConverters()).thenReturn(new ArrayList<>());

        ArgumentCaptor<HttpEntity> request = ArgumentCaptor.forClass(HttpEntity.class);
        Mockito.when(restTemplate.postForObject(url, request.capture(), String.class)).thenReturn(httpResult);

        myClass.callMethod(types, url);

        HttpEntity<String> actualHttpEntity = request.getValue();
        Assert.assertEquals(actualHttpEntity.getBody(), "");
    }
}

这是我对这个问题的解决方案,但它需要一些更改。

首先,您必须将 RestTemplate 外部化为一个 bean,并在其初始化中添加转换器。这样你就可以摆脱不覆盖那些转换器的问题。

@Bean
public RestTemplate restTemplate() {
    RestTemplate restTemplate = new RestTemplate();
    MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
    restTemplate.getMessageConverters().add(jsonConverter);
    StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
    restTemplate.getMessageConverters().add(stringConverter);
    return restTemplate;
}

这是包含 postJson 方法的新 class。如您所见,urlrestTemplate 是通过构造函数注入的。这样我们就可以测试不同的情况。

public class Poster {

    private RestTemplate restTemplate;
    private String url;

    public Poster(RestTemplate restTemplate, String url) {
        this.restTemplate = restTemplate;
        this.url = url;
    }

    public void postJson(Set<Type> types) {
        try {
            String oneString = types.stream().map(Type::toString).collect(Collectors.joining(","));
            Map<String, String> requestBody = new HashMap<>();
            requestBody.put("type", oneString);
            requestBody.put("data", "aws");
            JSONObject jsonObject = new JSONObject(requestBody);
            HttpEntity<String> request = new HttpEntity<>(jsonObject.toString(), null);

            ResponseEntity<String> result = restTemplate
                    .postForEntity(url, new HttpEntity<>(request, getHttpHeaders()), String.class);

            int code = result.getStatusCodeValue();
        } catch (Exception ignored) {}
    }

    private HttpHeaders getHttpHeaders() {
        return new HttpHeaders();
    }

}

这是该方法的测试 class。

class PosterTest {

    @Mock
    private RestTemplate restTemplate;

    private String url;
    private Poster poster;

    @BeforeEach
    public void setUp() {
        MockitoAnnotations.initMocks(this);

        this.url = "http://example.com/posturl";
        this.poster = new Poster(restTemplate, url);
    }

    @Test
    void postJson() {
        // set input, I used TreeSet just to have a sorted set
        // so that I can check the results against
        Set<Type> types = new TreeSet<>(Comparator.comparing(Type::toString));
        types.add(new Type("a"));
        types.add(new Type("b"));
        types.add(new Type("c"));
        types.add(new Type("d"));
        // response entity
        ResponseEntity<String> response = ResponseEntity.ok("RESPONSE");

        // mockito mock
        Mockito.when(restTemplate.postForEntity(
                ArgumentMatchers.eq(url),
                ArgumentMatchers.any(HttpHeaders.class),
                ArgumentMatchers.eq(String.class)
        )).thenReturn(response);

        // to check the results
        Map<String, String> requestBody = new HashMap<>();
        requestBody.put("type", "a,b,c,d");
        requestBody.put("data", "aws");
        JSONObject jsonObject = new JSONObject(requestBody);
        HttpEntity<String> request = new HttpEntity<>(jsonObject.toString(), null);
        HttpEntity<HttpEntity<String>> httpEntity = new HttpEntity<>(request, new HttpHeaders());

        // actual call
        poster.postJson(types);

        // verification
        Mockito.verify(restTemplate, times(1)).postForEntity(
                ArgumentMatchers.eq(url),
                ArgumentMatchers.eq(httpEntity),
                ArgumentMatchers.eq(String.class));
    }
}

最好有像 RestTemplate 或其他 ServiceClass 这样的依赖项,这样它们就可以很容易地被模拟和测试。

您正在测试 MyClass class 中的逻辑,因此您不应该模拟它。 RestTemplate 是 MyClass 中的依赖项,因此这正是您需要模拟的内容。一般来说,它在你的测试中应该是这样的:

这只是一个简单的例子。一个好的做法是检查传递给模拟的参数是否等于预期的参数。一种方法是将 Mockito.eq() 替换为实际预期数据。另一种是单独验证,像这样:

public ResponseEntity<String> postJson(Set<Type> Types){
            try {
                String oneString = String.join(",", Types);
               Map<String, String> requestBody = new HashMap<>();
                requestBody.put("type", oneString);
                JSONObject jsonObject = new JSONObject(requestBody);
                HttpEntity<String> request = new HttpEntity<String>(jsonObject.toString(), null);
    ResponseEntity result = restTemplate.exchange(url, HttpMethod.POST,
                        new HttpEntity<>(request, getHttpHeaders()), String.class);
                } 
        }
        return Types;

你可以为上面的方法编写测试如下

   @Mock
   RestTemplate restTemplate;

   private Poster poster;
   
   HttpEntity<String> request = new HttpEntity<>(jsonObject.toString(), getHttpHeaders());
   
   ResponseEntity<String> result = restTemplate.exchange(uri, HttpMethod.POST, request, String.class);
   
   Mockito.verify(restTemplate, Mockito.times(1)).exchange(
       Mockito.eq(uri),
       Mockito.eq(HttpMethod.POST),
       Mockito.eq(request),
       Mockito.eq(String.class));
   
   Assert.assertEquals(result, poster.postJson(mockData));
   
   HttpHeaders getHttpHeaders() {
       HttpHeaders headers = new HttpHeaders();
       headers.add(// whatever you need to add);
       return headers;
   }