1

我在名为“demo”的SpringBoot 应用程序中使用注解式 Resilience4j。通过 RestTemplate 调用外部后端时,我想使用 TimeLimiter 和 Retry 来实现以下目标:

  1. 将 REST 调用持续时间限制为 5 秒 --> 如果需要更长时间,则失败并显示 TimeoutException
  2. 重试 TimeoutException --> 最多尝试 2 次

为了查看我的弹性设置的配置是否按设计工作,我编写了一个集成测试。此测试在配置文件“test”下运行,并使用“application-test.yml”进行配置:

  1. 使用 TestRestTemplate 向我的“SimpleRestEndpointController”发送呼叫
  2. 控制器调用我的业务服务“CallExternalService”,它有一个带注释的方法“getPersonById”(注释:@TimeLimiter,@Retry)
  3. 通过这个方法,一个模拟的 RestTemplate 用于在“FANCY_URL”调用外部后端
  4. 使用 Mockito 对外部后端的 RestTemplate 调用减慢(使用 Thread.sleep)
  5. 我希望 TimeLimiter 在 5 秒后取消调用,并且 Retry 确保再次尝试 RestTemplate 调用(验证 RestTemplate 是否已被调用两次)

问题: TimeLimiter 和 Retry 已注册,但没有完成它们的工作(TimeLimiter 不限制调用持续时间)。因此 RestTemplate 只被调用一次,提供空的Person(见代码澄清)。可以克隆链接的示例项目并在运行测试时展示问题。

代码application-test.yml(也在这里:链接到 application-test.yml):

resilience4j:
  timelimiter:
    configs:
      default:
        timeoutDuration: 5s
        cancelRunningFuture: true
    instances:
      MY_RESILIENCE_KEY:
        baseConfig: default
  retry:
    configs:
      default:
        maxRetryAttempts: 2
        waitDuration: 100ms
        retryExceptions:
          - java.util.concurrent.TimeoutException
    instances:
      MY_RESILIENCE_KEY:
        baseConfig: default

此测试的代码(也在这里:链接到 IntegrationTest.java):

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {DemoApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@EnableAutoConfiguration
@ActiveProfiles("test")
public class IntegrationTest {
    private TestRestTemplate testRestTemplate;
    public final String FANCY_URL = "https://my-fancy-url-doesnt-matter.com/person";
    private String apiUrl;
    private HttpHeaders headers;
    
    @LocalServerPort
    private String localServerPort;
    
    @MockBean
    RestTemplate restTemplate;
    
    @Autowired
    CallExternalService callExternalService;
    
    @Autowired
    SimpleRestEndpointController simpleRestEndpointController;
    
    @Before
    public void setup() {
        this.headers = new HttpHeaders();
        this.testRestTemplate = new TestRestTemplate("username", "password");
        this.apiUrl = String.format("http://localhost:%s/person", localServerPort);
    }
    
    @Test
    public void testShouldRetryOnceWhenTimelimitIsReached() {
        // Arrange
        Person mockPerson = new Person();
        mockPerson.setId(1);
        mockPerson.setFirstName("First");
        mockPerson.setLastName("Last");
        ResponseEntity<Person> mockResponse = new ResponseEntity<>(mockPerson, HttpStatus.OK);
            
        
        Answer customAnswer = new Answer() {
            private int invocationCount = 0;
            @Override
            public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
                invocationCount++;
                if (invocationCount == 1) {
                    Thread.sleep(6000);
                    return new ResponseEntity<>(new Person(), HttpStatus.OK);
                } else {
                    return mockResponse;
                }
            }
        };
        
        doAnswer(customAnswer)
        .when(restTemplate).exchange(
                FANCY_URL,
                HttpMethod.GET,
                new HttpEntity<>(headers),
                new ParameterizedTypeReference<Person>() {});
        
        
        // Act
        ResponseEntity<Person> result = null;
        try {
            result = this.testRestTemplate.exchange(
                    apiUrl,
                    HttpMethod.GET,
                    new HttpEntity<>(headers),
                    new ParameterizedTypeReference<Person>() {
                    });
        } catch(Exception ex) {
            System.out.println(ex);         
        }
        
        
        // Assert
        verify(restTemplate, times(2)).exchange(
                FANCY_URL,
                HttpMethod.GET,
                new HttpEntity<>(headers),
                new ParameterizedTypeReference<Person>() {});
        Assert.assertNotNull(result);
        Assert.assertEquals(mockPerson, result.getBody());      
        
    }
}

我的应用程序代码展示了这个问题: https ://github.com/SidekickJohn/demo

我创建了一个“逻辑”的泳道图作为 README.md 的一部分:https ://github.com/SidekickJohn/demo/blob/main/README.md

4

1 回答 1

0

如果您想模拟您RestTemplate使用的真实 bean,则CallExternalService 必须使用 Mockito Spy -> https://www.baeldung.com/mockito-spy

但我通常更喜欢并建议使用 WireMock 而不是 Mockito 来模拟 HTTP 端点。

于 2020-12-02T07:59:46.107 回答