当前位置: 首页 > 知识库问答 >
问题:

用注释样式的Resilience4j测试SpringBoot

安承教
2023-03-14

我在我的SpringBoot应用程序“demo”中使用了注释样式的Resilience4j。当通过RestTemplate调用外部后端时,我希望使用TimeLimiter并重试以实现以下目标:

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

为了查看弹性设置的配置是否如设计的那样工作,我编写了一个IntegrationTest。该测试在概要文件“test”下运行,并使用“application-test.yml”进行配置:

  1. 使用TestRestTemplate向我的“SimpleRestendPointController”发送调用
  2. 控制器调用我的业务服务“CallExternalService”,它有一个带注释的方法“getPersonbyId”(注释:@timelimiter,@retry)
  3. 在此方法中,使用模拟的RestTemplate调用位于“fancy_url”的外部后端
  4. 使用Mockito会减慢对外部后端的RestTemplate调用(使用thread.sleep)
  5. 我希望时间限制程序在5秒后取消调用,重试确保再次尝试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

共有1个答案

阙星渊
2023-03-14

如果要模拟由CallExternalService使用的真正的RESTTemplatebean,则必须使用Mockito Spy->https://www.baeldung.com/Mockito-Spy

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

 类似资料:
  • 我正在尝试单元测试我的具有@PreAuthorize注释的API。我从智威汤逊得到了认知团体,并将其作为权威。我在api方法的预授权注释中检查了相同的内容。更新:我得到404 No mapping Delete profile/voluc3f5a_test.txt存在 测试类: 控制器方法:

  • 我想在Spring中测试注入依赖关系。 我想要一个这样的测试: 我尝试过使用ContextConfiguration和一个测试配置文件,但是测试失败了,我不想在测试中使用@autowired,我想创建我的类的一个实例,并且bean是自动autowired的。

  • 我想为上面的内容编写单元测试,以测试我正在使用的注释的sampleURL,比如如果我给出任何应该与regex模式匹配的URL。我浏览了以下链接:如何在spring中进行单元测试验证注释,如何使用JUnit测试类的验证注释?但它们没有多大帮助,我也有setSampleURL函数。那么,如何为sampleURL变量编写测试呢。基本上,我想为regex模式编写测试,即我给sampleURL的值是否与re

  • TestNG有一个很好的特性,可以将注释添加到测试类中(而不是测试方法)。根据文档,当类被注释时,类上所有公共的void返回方法都被视为测试方法。 更新:现在作为一个问题提出。

  • 是否可以通过注释在Spring Cloud Circuit Breaker上使用Resilience4j?我找不到任何关于它的留档,只有关于通过代码使用弹性4j的示例

  • 我是JMH的新手。在运行代码并使用不同的注释之后,我真的不明白它是如何工作的。我使用迭代=1、预热=1、fork=1来查看我的代码将执行一次,但事实并非如此。JMH运行我的代码超过100000次,我不知道为什么。那么,如何控制代码调用的时间?下面是我的代码:(我为测试修复了JMHSample\u 01)