java 运行 JUnit 测试时,Spring 的 @Retryable 不起作用

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/39478665/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-03 04:24:26  来源:igfitidea点击:

Spring's @Retryable not working when running JUnit Test

javaspringmavenjunitspring-retry

提问by Programmermatt

I have this test:

我有这个测试:

@RunWith(MockitoJUnitRunner.class)
public class myServiceTest {

@InjectMocks
myService subject;

private myService spy;

@Before
public void before() {
    spy = spy(subject);
}

@Test
public void testing() {

    when(spy.print2()).thenThrow(new RuntimeException()).thenThrow(new RuntimeException()).thenReturn("completed");
    spy.print1();
    verify(spy, times(3)).print2();
}

and then I have:

然后我有:

@Service("myService")
public class myService extends myAbstractServiceClass {


public String print1() {
    String temp = "";
    temp = print2();
    return temp;
}

 @Retryable
 public String print2() {
     return "completed";
 }
}

then I have this interface(which my abstractService implements):

然后我有这个接口(我的 abstractService 实现):

public interface myServiceInterface {

    @Retryable(maxAttempts = 3)
    String print1() throws RuntimeException;

    @Retryable(maxAttempts = 3)
    String print2() throws RuntimeException;

}

but, I get a runtimeexception thrown when I run the test, leading me to believe it is not retrying. Am I doing this wrong?

但是,我在运行测试时抛出了一个运行时异常,这让我相信它没有重试。我这样做错了吗?

回答by Johannes Leimer

This is because you are not using the SpringJUnitClassRunner.

这是因为您没有使用SpringJUnitClassRunner.

Mockito and your own classes are not taking the @Retryableannotation in account. So you rely on the implementation of Spring to do so. But your test does not activate Spring.

Mockito 和您自己的类没有考虑@Retryable注释。所以你依靠 Spring 的实现来做到这一点。但是您的测试不会激活 Spring。

This is from the SpringJUnit4ClassRunner JavaDoc:

这是来自 SpringJUnit4ClassRunner JavaDoc:

SpringJUnit4ClassRunner is a custom extension of JUnit's BlockJUnit4ClassRunner which provides functionality of the Spring TestContext Framework to standard JUnit tests by means of the TestContextManager and associated support classes and annotations. To use this class, simply annotate a JUnit 4 based test class with @RunWith(SpringJUnit4ClassRunner.class) or @RunWith(SpringRunner.class).

SpringJUnit4ClassRunner 是 JUnit 的 BlockJUnit4ClassRunner 的自定义扩展,它通过 TestContextManager 和相关的支持类和注释为标准 JUnit 测试提供 Spring TestContext Framework 的功能。要使用这个类,只需使用 @RunWith(SpringJUnit4ClassRunner.class) 或 @RunWith(SpringRunner.class) 注释一个基于 JUnit 4 的测试类。

You should restructure your test class at least to something like:

您应该至少将测试类重组为:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=MyConfig.class)
public class MyServiceTest {
    @Configuration
    @EnableRetry
    @Import(myService.class)
    public static class MyConfig {}
...

What am I doing there?

我在那里做什么?

  1. activate the Spring JUnit hook
  2. specify the Spring context configuration class
  3. define the spring configuration and import your service as a bean
  4. enable the retryable annotation
  1. 激活 Spring JUnit 钩子
  2. 指定Spring上下文配置类
  3. 定义 spring 配置并将您的服务作为 bean 导入
  4. 启用可重试注释

Are there some other pitfalls?

还有其他一些陷阱吗?

  • Yes, you are using Mockito to simulate an exception. If you want to test this behaviour with Spring like this, you should have a look at Springockito Annotations.
  • But be aware of that: Springockito you will replace the spring bean completely which forces you to proxy the call of your retryable. You need a structure like: test -> retryableService -> exceptionThrowingBean. Then you can use Springockito or what ever you like e.g. ReflectionTestUtilsto configure the exceptionThrowingBeanwith the behaviour you like.
  • You should reference the interface type of your service in your test: MyServiceInterface
  • And last but not least. There is a naming convention nearly all Java developers follow: class names have first letter of each internal word capitalized
  • 是的,您正在使用 Mockito 来模拟异常。如果你想像这样用 Spring 测试这种行为,你应该看看Springockito Annotations
  • 但请注意:Springockito 您将完全替换 spring bean,这迫使您代理可重试的调用。你需要如下的结构:test -> retryableService -> exceptionThrowingBean。然后你可以使用 Springockito 或任何你喜欢的东西,例如用你喜欢的行为ReflectionTestUtils来配置exceptionThrowingBean
  • 您应该在测试中引用服务的接口类型: MyServiceInterface
  • 最后但并非最不重要。几乎所有 Java 开发人员都遵循一个命名约定:类名有first letter of each internal word capitalized

Hope that helps.

希望有帮助。

回答by isaolmez

I think you should let Spring manage the bean, create the appropriate proxy and handle the process. If you want to mock specific beans, you can create mocks and inject them to the service under test.

我认为您应该让 Spring 管理 bean,创建适当的代理并处理该过程。如果你想模拟特定的 bean,你可以创建模拟并将它们注入到被测服务中。

1st option could be unwrapping proxied service, creating mocks and manually injecting them:

第一个选项可能是解包代理服务,创建模拟并手动注入它们:

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {RetryConfiguration.class})
@DirtiesContext
public class TheServiceImplTest {

    @Autowired
    private TheService theService;

    @Before
    public void setUp(){
        TheService serviceWithoutProxy = AopTestUtils.getUltimateTargetObject(theService);
        RetryProperties mockRetryProperties = Mockito.mock(RetryProperties.class);
        ReflectionTestUtils.setField(serviceWithoutProxy, "retryProperties", mockRetryProperties);
    }

    @Test
    public void shouldFetch() {
        Assert.assertNotNull(theService);
    }
}

In this example, I mocked one bean, RetryProperties, and injected into the service. Also note that, in this approach you are modifying the test application context which is cached by Spring. This means that if you don't use @DirtiesContext, service will continue its way with mocked bean in other tests. You can read more here

在这个例子中,我模拟了一个 bean,RetryProperties,并注入到服务中。另请注意,在这种方法中,您正在修改由 Spring 缓存的测试应用程序上下文。这意味着如果您不使用@DirtiesContext,服务将在其他测试中使用模拟 bean 继续其方式。你可以在这里阅读更多

Second option would be creating a test specific @Configuration and mock the depended bean there. Spring will pick up this new mocked bean instead of the original one:

第二个选项是创建一个特定于测试的 @Configuration 并在那里模拟依赖的 bean。Spring 将选择这个新的模拟 bean 而不是原来的 bean:

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {RetryConfiguration.class, TheServiceImplSecondTest.TestConfiguration.class})
public class TheServiceImplSecondTest {

    @Autowired
    private TheService theService;

    @Test
    public void shouldFetch() {
        Assert.assertNotNull(theService);
    }

    @Configuration
    static class TestConfiguration {
        @Bean
        public RetryProperties retryProperties() {
            return Mockito.mock(RetryProperties.class);
        }
    }
}

In this example, we have defined a test specific configuration and added it to the @ContextConfiguration.

在这个例子中,我们定义了一个测试特定的配置并将它添加到@ContextConfiguration。

回答by Justas

Another way:

其他方式:

@EnableRetry
@RunWith(SpringRunner.class)
@SpringBootTest(classes={ServiceToTest.class})
public class RetryableTest {

    @Autowired
    private ServiceToTest serviceToTest;

    @MockBean
    private ComponentInsideTestClass componentInsideTestClass;

    @Test
    public void retryableTest(){
        serviceToTest.method();
    }

}