【问题标题】:Testing SpringBoot with annotation-style Resilience4j使用注解式 Resilience4j 测试 SpringBoot
【发布时间】:2021-03-14 03:37:23
【问题描述】:

我正在使用注释样式的 Resilience4j 和我的 SpringBoot app 称为“演示”。通过 RestTemplate 调用外部后端时,我想使用 TimeLimiter 和 Retry 来实现以下目标:

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

为了查看我的弹性设置的配置是否按设计工作,我编写了一个集成测试。该测试在配置文件“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 的代码(也在这里:Link to 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

这个测试的代码(也在这里:Link to 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

【问题讨论】:

    标签: spring-boot resilience4j resilience4j-retry


    【解决方案1】:

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

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

    【讨论】:

    • 嗯。我确实明白模拟和间谍之间的区别。实际上,我在这里更喜欢模拟。我不希望 RestTemplate 执行实际调用。如上所述,我想“拦截”交换方法(使用“doAnswer(...)when(restTemplate).exchange)。
    • 另外:当我为 RestTemplate 使用 @MockBean 注释时,@Autowired 的 CallExternalService 应该注入这个 Mock。这似乎有效。测试执行后的失败消息表明确实存在与此模拟的交互,但它只是 1 次交互而不是 2 次。
    • 好的,我刚刚看到您的 CallExternalService 没有实现接口。 Spring AOP 使用 JDK 动态代理或 CGLIB 为给定的目标对象创建代理。 (当您有选择时,首选 JDK 动态代理)。如果要代理的目标对象至少实现一个接口,则将使用 JDK 动态代理。如果目标对象没有实现任何接口,那么将创建一个 CGLIB 代理。我们的 Aspects 还没有经过 CGLIB 测试。我们建议实现接口。能否请您对其进行测试并检查是否调用了 RetryAspect.proceed?
    • 我会尽快检查出来。将在此处为您提供反馈。感谢您的提示!
    • @RobertWinkler 我在尝试测试 TimeLimiter 功能时遇到了同样的问题。不幸的是,引入界面没有帮助,TimeLimiterAspect.proceed 没有被接受。
    猜你喜欢
    • 2017-02-05
    • 2017-06-23
    • 1970-01-01
    • 2021-09-25
    • 2020-01-17
    • 2020-10-22
    • 2020-02-11
    • 2021-09-23
    • 2020-09-02
    相关资源
    最近更新 更多